diff --git a/.eslintrc.js b/.eslintrc.js index 8cc33f62ab94a..2729411b7e10b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,7 +47,7 @@ module.exports = { 'plugin:vue/recommended', 'standard', 'plugin:jsonc/recommended-with-json', - // 'plugin:vuejs-accessibility/recommended' // uncomment once issues are fixed + 'plugin:vuejs-accessibility/recommended' ], // https://eslint.org/docs/user-guide/configuring#configuring-plugins @@ -70,6 +70,7 @@ module.exports = { some: ['nesting', 'id'] } }], + 'vuejs-accessibility/no-static-element-interactions': 'off', 'n/no-callback-literal': 'warn', 'n/no-path-concat': 'warn', 'unicorn/better-regex': 'error', diff --git a/.github/workflows/autoMerge.yml b/.github/workflows/autoMerge.yml index a5391e7245551..c979c741475ad 100644 --- a/.github/workflows/autoMerge.yml +++ b/.github/workflows/autoMerge.yml @@ -9,7 +9,7 @@ jobs: steps: - name: Auto Merge PR - if: github.event.pull_request.draft == false && (contains(${{ github.event.pull_request.base.ref }}, 'development') || contains(${{ github.event.pull_request.base.ref }}, 'RC')) + if: ${{ !github.event.pull_request.draft && (contains(github.event.pull_request.base.ref, 'development') || contains(github.event.pull_request.base.ref, 'RC')) }} run: | echo ${{ secrets.PUSH_TOKEN }} >> auth.txt gh auth login --with-token < auth.txt diff --git a/package.json b/package.json index 02e6b398259ef..fb0de3ae41601 100644 --- a/package.json +++ b/package.json @@ -54,16 +54,16 @@ "ci": "yarn install --silent --frozen-lockfile" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.4.0", - "@fortawesome/free-brands-svg-icons": "^6.4.0", - "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-brands-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/vue-fontawesome": "^2.0.10", + "@seald-io/nedb": "^4.0.2", "@silvermine/videojs-quality-selector": "^1.3.0", "autolinker": "^4.0.0", "electron-context-menu": "^3.6.1", "lodash.debounce": "^4.0.8", - "marked": "^4.3.0", - "nedb-promises": "^6.2.1", + "marked": "^7.0.2", "path-browserify": "^1.0.1", "process": "^0.11.10", "video.js": "7.21.5", @@ -81,10 +81,10 @@ "youtubei.js": "^5.8.0" }, "devDependencies": { - "@babel/core": "^7.22.9", - "@babel/eslint-parser": "^7.22.9", + "@babel/core": "^7.22.10", + "@babel/eslint-parser": "^7.22.10", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.22.9", + "@babel/preset-env": "^7.22.10", "@double-great/stylelint-a11y": "^2.0.2", "babel-loader": "^9.1.3", "copy-webpack-plugin": "^11.0.0", @@ -92,8 +92,8 @@ "css-minimizer-webpack-plugin": "^5.0.1", "electron": "^22.3.18", "electron-builder": "^24.6.3", - "eslint": "^8.46.0", - "eslint-config-prettier": "^8.9.0", + "eslint": "^8.47.0", + "eslint-config-prettier": "^9.0.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-import": "^2.28.0", "eslint-plugin-jsonc": "^2.9.0", @@ -101,25 +101,25 @@ "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-unicorn": "^48.0.1", - "eslint-plugin-vue": "^9.16.1", - "eslint-plugin-vuejs-accessibility": "^2.1.0", + "eslint-plugin-vue": "^9.17.0", + "eslint-plugin-vuejs-accessibility": "^2.2.0", "eslint-plugin-yml": "^1.8.0", "html-webpack-plugin": "^5.5.3", "js-yaml": "^4.1.0", "json-minimizer-webpack-plugin": "^4.0.0", - "lefthook": "^1.4.7", + "lefthook": "^1.4.8", "mini-css-extract-plugin": "^2.7.6", "npm-run-all": "^4.1.5", "postcss": "^8.4.26", "postcss-scss": "^4.0.6", "prettier": "^2.8.8", "rimraf": "^5.0.1", - "sass": "^1.64.1", + "sass": "^1.65.1", "sass-loader": "^13.3.2", "stylelint": "^15.10.2", "stylelint-config-sass-guidelines": "^10.0.0", "stylelint-config-standard": "^34.0.0", - "stylelint-high-performance-animation": "^1.8.0", + "stylelint-high-performance-animation": "^1.9.0", "tree-kill": "1.2.2", "vue-devtools": "^5.1.4", "vue-eslint-parser": "^9.3.1", diff --git a/src/constants.js b/src/constants.js index cc82cdcce9cf0..f4286c205e0de 100644 --- a/src/constants.js +++ b/src/constants.js @@ -71,6 +71,11 @@ const SyncEvents = { } } +// https://v2.vuejs.org/v2/api/#provide-inject +const Injectables = { + SHOW_OUTLINES: 'showOutlines' +} + // Utils const MAIN_PROFILE_ID = 'allChannels' @@ -78,5 +83,6 @@ export { IpcChannels, DBActions, SyncEvents, + Injectables, MAIN_PROFILE_ID } diff --git a/src/datastores/handlers/base.js b/src/datastores/handlers/base.js index 89300feb9d740..466c970a8b4c5 100644 --- a/src/datastores/handlers/base.js +++ b/src/datastores/handlers/base.js @@ -2,17 +2,21 @@ import db from '../index' class Settings { static find() { - return db.settings.find({ _id: { $ne: 'bounds' } }) + return db.settings.findAsync({ _id: { $ne: 'bounds' } }) } static upsert(_id, value) { - return db.settings.update({ _id }, { _id, value }, { upsert: true }) + return db.settings.updateAsync({ _id }, { _id, value }, { upsert: true }) + } + + static persist() { + return db.settings.compactDatafileAsync() } // ******************** // // Unique Electron main process handlers static _findAppReadyRelatedSettings() { - return db.settings.find({ + return db.settings.findAsync({ $or: [ { _id: 'disableSmoothScrolling' }, { _id: 'useProxy' }, @@ -24,96 +28,96 @@ class Settings { } static _findBounds() { - return db.settings.findOne({ _id: 'bounds' }) + return db.settings.findOneAsync({ _id: 'bounds' }) } static _findTheme() { - return db.settings.findOne({ _id: 'baseTheme' }) + return db.settings.findOneAsync({ _id: 'baseTheme' }) } static _findSidenavSettings() { return { - hideTrendingVideos: db.settings.findOne({ _id: 'hideTrendingVideos' }), - hidePopularVideos: db.settings.findOne({ _id: 'hidePopularVideos' }), - backendFallback: db.settings.findOne({ _id: 'backendFallback' }), - backendPreference: db.settings.findOne({ _id: 'backendPreference' }), - hidePlaylists: db.settings.findOne({ _id: 'hidePlaylists' }), + hideTrendingVideos: db.settings.findOneAsync({ _id: 'hideTrendingVideos' }), + hidePopularVideos: db.settings.findOneAsync({ _id: 'hidePopularVideos' }), + backendFallback: db.settings.findOneAsync({ _id: 'backendFallback' }), + backendPreference: db.settings.findOneAsync({ _id: 'backendPreference' }), + hidePlaylists: db.settings.findOneAsync({ _id: 'hidePlaylists' }), } } static _updateBounds(value) { - return db.settings.update({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true }) + return db.settings.updateAsync({ _id: 'bounds' }, { _id: 'bounds', value }, { upsert: true }) } // ******************** // } class History { static find() { - return db.history.find({}).sort({ timeWatched: -1 }) + return db.history.findAsync({}).sort({ timeWatched: -1 }) } static upsert(record) { - return db.history.update({ videoId: record.videoId }, record, { upsert: true }) + return db.history.updateAsync({ videoId: record.videoId }, record, { upsert: true }) } static updateWatchProgress(videoId, watchProgress) { - return db.history.update({ videoId }, { $set: { watchProgress } }, { upsert: true }) + return db.history.updateAsync({ videoId }, { $set: { watchProgress } }, { upsert: true }) } static updateLastViewedPlaylist(videoId, lastViewedPlaylistId, lastViewedPlaylistType) { - return db.history.update({ videoId }, { $set: { lastViewedPlaylistId, lastViewedPlaylistType } }, { upsert: true }) + return db.history.updateAsync({ videoId }, { $set: { lastViewedPlaylistId, lastViewedPlaylistType } }, { upsert: true }) } static delete(videoId) { - return db.history.remove({ videoId }) + return db.history.removeAsync({ videoId }) } static deleteAll() { - return db.history.remove({}, { multi: true }) + return db.history.removeAsync({}, { multi: true }) } static persist() { - db.history.persistence.compactDatafile() + return db.history.compactDatafileAsync() } } class Profiles { static create(profile) { - return db.profiles.insert(profile) + return db.profiles.insertAsync(profile) } static find() { - return db.profiles.find({}) + return db.profiles.findAsync({}) } static upsert(profile) { - return db.profiles.update({ _id: profile._id }, profile, { upsert: true }) + return db.profiles.updateAsync({ _id: profile._id }, profile, { upsert: true }) } static delete(id) { - return db.profiles.remove({ _id: id }) + return db.profiles.removeAsync({ _id: id }) } static persist() { - db.profiles.persistence.compactDatafile() + return db.profiles.compactDatafileAsync() } } class Playlists { static create(playlists) { - return db.playlists.insert(playlists) + return db.playlists.insertAsync(playlists) } static find() { - return db.playlists.find({}) + return db.playlists.findAsync({}) } static upsert(playlist) { - return db.playlists.update({ _id: playlist._id }, playlist, { upsert: true }) + return db.playlists.updateAsync({ _id: playlist._id }, playlist, { upsert: true }) } static upsertVideoByPlaylistId(_id, videoData) { - return db.playlists.update( + return db.playlists.updateAsync( { _id }, { $push: { videos: videoData } }, { upsert: true } @@ -121,7 +125,7 @@ class Playlists { } static upsertVideoIdsByPlaylistId(_id, videoIds) { - return db.playlists.update( + return db.playlists.updateAsync( { _id }, { $push: { videos: { $each: videoIds } } }, { upsert: true } @@ -129,11 +133,11 @@ class Playlists { } static delete(_id) { - return db.playlists.remove({ _id, protected: { $ne: true } }) + return db.playlists.removeAsync({ _id, protected: { $ne: true } }) } static deleteVideoIdByPlaylistId(_id, videoId) { - return db.playlists.update( + return db.playlists.updateAsync( { _id }, { $pull: { videos: { videoId } } }, { upsert: true } @@ -141,7 +145,7 @@ class Playlists { } static deleteVideoIdsByPlaylistId(_id, videoIds) { - return db.playlists.update( + return db.playlists.updateAsync( { _id }, { $pull: { videos: { $in: videoIds } } }, { upsert: true } @@ -149,7 +153,7 @@ class Playlists { } static deleteAllVideosByPlaylistId(_id) { - return db.playlists.update( + return db.playlists.updateAsync( { _id }, { $set: { videos: [] } }, { upsert: true } @@ -157,19 +161,34 @@ class Playlists { } static deleteMultiple(ids) { - return db.playlists.remove({ _id: { $in: ids }, protected: { $ne: true } }) + return db.playlists.removeAsync({ _id: { $in: ids }, protected: { $ne: true } }) } static deleteAll() { - return db.playlists.removeMany() + return db.playlists.removeAsync({}, { multi: true }) } + + static persist() { + return db.playlists.compactDatafileAsync() + } +} + +function compactAllDatastores() { + return Promise.allSettled([ + Settings.persist(), + History.persist(), + Profiles.persist(), + Playlists.persist(), + ]) } const baseHandlers = { settings: Settings, history: History, profiles: Profiles, - playlists: Playlists + playlists: Playlists, + + compactAllDatastores, } export default baseHandlers diff --git a/src/datastores/index.js b/src/datastores/index.js index 644981634e4c0..a7038fec96651 100644 --- a/src/datastores/index.js +++ b/src/datastores/index.js @@ -1,4 +1,4 @@ -import Datastore from 'nedb-promises' +import Datastore from '@seald-io/nedb' let dbPath = null @@ -23,9 +23,9 @@ if (process.env.IS_ELECTRON_MAIN) { } const db = {} -db.settings = Datastore.create({ filename: dbPath('settings'), autoload: true }) -db.profiles = Datastore.create({ filename: dbPath('profiles'), autoload: true }) -db.playlists = Datastore.create({ filename: dbPath('playlists'), autoload: true }) -db.history = Datastore.create({ filename: dbPath('history'), autoload: true }) +db.settings = new Datastore({ filename: dbPath('settings'), autoload: true }) +db.profiles = new Datastore({ filename: dbPath('profiles'), autoload: true }) +db.playlists = new Datastore({ filename: dbPath('playlists'), autoload: true }) +db.history = new Datastore({ filename: dbPath('history'), autoload: true }) export default db diff --git a/src/main/index.js b/src/main/index.js index 0dc9d920d6f6e..d5ddbc79144e3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -289,7 +289,7 @@ function runApp() { // InnerTube rejects requests if the referer isn't YouTube or empty const innertubeAndMediaRequestFilter = { urls: ['https://www.youtube.com/youtubei/*', 'https://*.googlevideo.com/videoplayback?*'] } - session.defaultSession.webRequest.onBeforeSendHeaders(innertubeAndMediaRequestFilter, ({ requestHeaders, url }, callback) => { + session.defaultSession.webRequest.onBeforeSendHeaders(innertubeAndMediaRequestFilter, ({ requestHeaders, url, resourceType }, callback) => { requestHeaders.Referer = 'https://www.youtube.com/' requestHeaders.Origin = 'https://www.youtube.com' @@ -300,6 +300,38 @@ function runApp() { delete requestHeaders['Content-Type'] } + // YouTube throttles the adaptive formats if you request a chunk larger than 10MiB. + // For the DASH formats we are fine as video.js doesn't seem to ever request chunks that big. + // The legacy formats don't have any chunk size limits. + // For the audio formats we need to handle it ourselves, as the browser requests the entire audio file, + // which means that for most videos that are loger than 10 mins, we get throttled, as the audio track file sizes surpass that 10MiB limit. + + // This code checks if the file is larger than the limit, by checking the `clen` query param, + // which YouTube helpfully populates with the content length for us. + // If it does surpass that limit, it then checks if the requested range is larger than the limit + // (seeking right at the end of the video, would result in a small enough range to be under the chunk limit) + // if that surpasses the limit too, it then limits the requested range to 10MiB, by setting the range to `start-${start + 10MiB}`. + if (resourceType === 'media' && url.includes('&mime=audio') && requestHeaders.Range) { + const TEN_MIB = 10 * 1024 * 1024 + + const contentLength = parseInt(new URL(url).searchParams.get('clen')) + + if (contentLength > TEN_MIB) { + const [startStr, endStr] = requestHeaders.Range.split('=')[1].split('-') + + const start = parseInt(startStr) + + // handle open ended ranges like `0-` and `1234-` + const end = endStr.length === 0 ? contentLength : parseInt(endStr) + + if (end - start > TEN_MIB) { + const newEnd = start + TEN_MIB + + requestHeaders.Range = `bytes=${start}-${newEnd}` + } + } + } + // eslint-disable-next-line n/no-callback-literal callback({ requestHeaders }) }) @@ -1008,28 +1040,33 @@ function runApp() { // ************************************************* // app.on('window-all-closed', () => { - // Clear cache and storage if it's the last window - session.defaultSession.clearCache() - session.defaultSession.clearStorageData({ - storages: [ - 'appcache', - 'cookies', - 'filesystem', - 'indexdb', - 'shadercache', - 'websql', - 'serviceworkers', - 'cachestorage' - ] + // Clean up resources (datastores' compaction + Electron cache and storage data clearing) + cleanUpResources().finally(() => { + if (process.platform !== 'darwin') { + app.quit() + } }) - - // For MacOS the app would still "run in background" - // and create new window on event `activate` - if (process.platform !== 'darwin') { - app.quit() - } }) + function cleanUpResources() { + return Promise.allSettled([ + baseHandlers.compactAllDatastores(), + session.defaultSession.clearCache(), + session.defaultSession.clearStorageData({ + storages: [ + 'appcache', + 'cookies', + 'filesystem', + 'indexdb', + 'shadercache', + 'websql', + 'serviceworkers', + 'cachestorage' + ] + }) + ]) + } + // MacOS event // https://www.electronjs.org/docs/latest/api/app#event-activate-macos app.on('activate', () => { diff --git a/src/renderer/App.js b/src/renderer/App.js index b480b92d04a11..58c566ab83366 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -12,7 +12,7 @@ import FtProgressBar from './components/ft-progress-bar/ft-progress-bar.vue' import FtPlaylistAddVideoPrompt from './components/ft-playlist-add-video-prompt/ft-playlist-add-video-prompt.vue' import FtCreatePlaylistPrompt from './components/ft-create-playlist-prompt/ft-create-playlist-prompt.vue' import { marked } from 'marked' -import { IpcChannels } from '../constants' +import { Injectables, IpcChannels } from '../constants' import packageDetails from '../../package.json' import { openExternalLink, openInternalPath, showToast } from './helpers/utils' @@ -34,6 +34,11 @@ export default defineComponent({ FtPlaylistAddVideoPrompt, FtCreatePlaylistPrompt, }, + provide: function () { + return { + [Injectables.SHOW_OUTLINES]: this.showOutlines + } + }, data: function () { return { dataReady: false, @@ -505,6 +510,15 @@ export default defineComponent({ } }, + /** + * provided to all child components, see `provide` near the top of this file + * after injecting it, they can show outlines during keyboard navigation + * e.g. cycling through tabs with the arrow keys + */ + showOutlines: function () { + this.hideOutlines = false + }, + ...mapMutations([ 'setInvidiousInstancesList' ]), diff --git a/src/renderer/App.vue b/src/renderer/App.vue index 2e7ae23b9340c..791b771ceaf42 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -40,7 +40,6 @@ diff --git a/src/renderer/components/data-settings/data-settings.js b/src/renderer/components/data-settings/data-settings.js index 48ede5c6247d6..9c12f33aa9b3e 100644 --- a/src/renderer/components/data-settings/data-settings.js +++ b/src/renderer/components/data-settings/data-settings.js @@ -1162,10 +1162,8 @@ export default defineComponent({ ...mapActions([ 'updateProfile', - 'compactProfiles', 'updateShowProgressBar', 'updateHistory', - 'compactHistory', 'addPlaylist', 'addVideo' ]), diff --git a/src/renderer/components/ft-list-channel/ft-list-channel.vue b/src/renderer/components/ft-list-channel/ft-list-channel.vue index cfc85205005e1..cd034322cbae0 100644 --- a/src/renderer/components/ft-list-channel/ft-list-channel.vue +++ b/src/renderer/components/ft-list-channel/ft-list-channel.vue @@ -10,10 +10,12 @@
diff --git a/src/renderer/components/ft-list-video/ft-list-video.vue b/src/renderer/components/ft-list-video/ft-list-video.vue index 6baef0bd8888c..6ae9894ea266d 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -23,6 +23,7 @@
0 ? Array.from(this.profileName)[0].toUpperCase() : '' } }, methods: { - goToProfile: function () { + goToProfile: function (event) { + if (event instanceof KeyboardEvent) { + if (event.target.getAttribute('role') === 'link' && event.key !== 'Enter') { + return + } + } this.$router.push({ path: `/settings/profile/edit/${this.profileId}` }) diff --git a/src/renderer/components/ft-profile-bubble/ft-profile-bubble.vue b/src/renderer/components/ft-profile-bubble/ft-profile-bubble.vue index 6879570dd057d..cfed977e68996 100644 --- a/src/renderer/components/ft-profile-bubble/ft-profile-bubble.vue +++ b/src/renderer/components/ft-profile-bubble/ft-profile-bubble.vue @@ -1,7 +1,11 @@