diff --git a/.eslintrc.js b/.eslintrc.js index edd417b..5078587 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ const unitTestsExtends = ['plugin:ava/recommended'] -const cypressTestsExtends = ['plugin:cypress/recommended', 'eslint-config-sinon', 'plugin:chai-friendly/recommended'] +const cypressTestsExtends = ['plugin:cypress/recommended', 'plugin:chai-friendly/recommended'] const commonExtends = ['plugin:prettier/recommended', 'plugin:unicorn/recommended', 'plugin:sonarjs/recommended'] const tsExtends = ['airbnb-typescript/base', ...commonExtends] const jsExtends = ['airbnb-base', ...commonExtends] @@ -60,6 +60,7 @@ const TS_OVERRIDE = { parser: '@typescript-eslint/parser', parserOptions: { project: './tsconfig.eslint.json', + // eslint-disable-next-line unicorn/prefer-module tsconfigRootDir: __dirname, }, plugins: ['@typescript-eslint'], @@ -72,6 +73,7 @@ const TS_OVERRIDE = { overrides: [UNIT_TESTS_TS_OVERRIDE, CYPRESS_TS_OVERRIDE], } +// eslint-disable-next-line unicorn/prefer-module module.exports = { extends: jsExtends, overrides: [UNIT_TESTS_JS_OVERRIDE, CYPRESS_JS_OVERRIDE, TS_OVERRIDE], diff --git a/.nycrc b/.nycrc index bfbc2df..a0b2edf 100644 --- a/.nycrc +++ b/.nycrc @@ -1,18 +1,9 @@ { "extends": "@istanbuljs/nyc-config-typescript", "check-coverage": true, - "include": [ - "src/**", - "metadata.ts" - ], - "exclude": [ - "dist/**", - "cypress/**", - "test/**" - ], - "reporter": [ - "text" - ], + "include": ["src/**", "metadata.ts"], + "exclude": ["dist/**", "cypress/**", "test/**"], + "reporter": ["text"], "lines": 70, "branches": 70, "statements": 70 diff --git a/CHANGELOG.md b/CHANGELOG.md index 235d379..fe665f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ ## [1.7.1](https://github.com/avallete/yt-playlists-delete-enhancer/compare/v1.7.0...v1.7.1) (2024-05-12) - ### Bug Fixes -* api continuation requests ([#242](https://github.com/avallete/yt-playlists-delete-enhancer/issues/242)) ([87c238b](https://github.com/avallete/yt-playlists-delete-enhancer/commit/87c238b4d25777172d99f7c80475563cbae6e3ec)) +- api continuation requests ([#242](https://github.com/avallete/yt-playlists-delete-enhancer/issues/242)) ([87c238b](https://github.com/avallete/yt-playlists-delete-enhancer/commit/87c238b4d25777172d99f7c80475563cbae6e3ec)) # [1.7.0](https://github.com/avallete/yt-playlists-delete-enhancer/compare/v1.6.2...v1.7.0) (2024-05-06) diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 1d7b148..f5f281c 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/prefer-module */ /// // *********************************************************** // This example plugins/index.js can be used to load plugins @@ -12,7 +13,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) // eslint-disable-next-line import/no-extraneous-dependencies -const path = require('path') +const path = require('node:path') const DIST_PATH = path.resolve(path.join(__dirname, '..', '..', 'dist')) diff --git a/src/components/remove-video-enhancer-app.tsx b/src/components/remove-video-enhancer-app.tsx index 76767be..3c1f485 100644 --- a/src/components/remove-video-enhancer-app.tsx +++ b/src/components/remove-video-enhancer-app.tsx @@ -40,11 +40,15 @@ export default class RemoveVideoEnhancerApp extends Component await removeWatchHistoryForVideo(this.props.config, videoId) removeWatchedFromPlaylistUI(videoId) const { playlist } = this.state - playlist?.continuations[0].videos.forEach((v) => { - if (v.videoId === videoId) { - v.percentDurationWatched = 0 + if (playlist) { + for (const v of playlist.continuations[0].videos) { + if (v.videoId === videoId) { + v.percentDurationWatched = 0 + } } - }) + } else { + throw new Error('Playlist not found') + } } catch (error) { this.setState({ ...this.state, errorMessages: [(error as Error).message] }) } @@ -55,7 +59,7 @@ export default class RemoveVideoEnhancerApp extends Component if (playlist && playlist.continuations[0].videos.length > 0) { const [toDeleteVideos, toKeepVideos] = partition( playlist.continuations[0].videos, - (v) => v.percentDurationWatched >= watchTimeValue + (v) => v.percentDurationWatched >= watchTimeValue, ) if (toDeleteVideos.length > 0) { try { @@ -88,10 +92,7 @@ export default class RemoveVideoEnhancerApp extends Component } shouldComponentUpdate(nextProperties: Properties) { - if (nextProperties.playlistName !== this.state?.playlist?.playlistId) { - return true - } - return false + return nextProperties.playlistName !== this.state?.playlist?.playlistId } async componentDidUpdate(previousProperties: Properties) { diff --git a/src/components/remove-video-enhancer-container.tsx b/src/components/remove-video-enhancer-container.tsx index de4a81f..9dd0a30 100644 --- a/src/components/remove-video-enhancer-container.tsx +++ b/src/components/remove-video-enhancer-container.tsx @@ -22,10 +22,7 @@ export const REMOVE_BUTTON_ALT = 'Remove button to start removing videos' function validate(value: any): boolean { const numberValue = Number(value) - if (Number.isSafeInteger(numberValue) && numberValue >= 0 && numberValue <= 100) { - return true - } - return false + return !!(Number.isSafeInteger(numberValue) && numberValue >= 0 && numberValue <= 100) } function RemoveVideoEnhancerContainer({ @@ -70,7 +67,7 @@ function RemoveVideoEnhancerContainer({ onClick: removeVideo, }), ], - element + element, ) } diff --git a/src/lib/list-map-search.ts b/src/lib/list-map-search.ts index 10b7a16..c7fa38a 100644 --- a/src/lib/list-map-search.ts +++ b/src/lib/list-map-search.ts @@ -15,7 +15,7 @@ export default function listMapSearch( needles: Array, haystack: Array, needleKeyGetter: (item: T) => K, - haystackKeyGetter: (item: U) => K + haystackKeyGetter: (item: U) => K, ): Record | false { const searchMap: Record = {} as Record // We cannot found all our needles into our haystack diff --git a/src/operations/actions/remove-videos-from-playlist-ui.ts b/src/operations/actions/remove-videos-from-playlist-ui.ts index 3f124d8..f6b92a1 100644 --- a/src/operations/actions/remove-videos-from-playlist-ui.ts +++ b/src/operations/actions/remove-videos-from-playlist-ui.ts @@ -7,6 +7,7 @@ export function removeVideoFromPlaylistUI(videoId: string) { removeVideosFromPlaylist([{ videoId, percentDurationWatched: 100 }]) decrementNumberOfVideosInPlaylist(1) } catch (error) { + // eslint-disable-next-line no-console console.error(error) // If an error occurs while trying to dynamically update the UI // reload the page to update the UI @@ -19,6 +20,7 @@ export default function removeVideosFromPlaylistUI(toDeleteVideos: PlaylistVideo removeVideosFromPlaylist(toDeleteVideos) decrementNumberOfVideosInPlaylist(toDeleteVideos.length) } catch (error) { + // eslint-disable-next-line no-console console.error(error) // If an error occurs while trying to dynamically update the UI // reload the page to update the UI diff --git a/src/operations/conditions/is-on-playlist-page.ts b/src/operations/conditions/is-on-playlist-page.ts index eac7a35..15df6ba 100644 --- a/src/operations/conditions/is-on-playlist-page.ts +++ b/src/operations/conditions/is-on-playlist-page.ts @@ -4,10 +4,7 @@ const PLAYLIST_URL_PATHNAME = '/playlist' const isOnPlaylistPage: Condition = (window_: Window): boolean => { const url = new URL(window_.location.href) - if (url.pathname === PLAYLIST_URL_PATHNAME) { - return true - } - return false + return url.pathname === PLAYLIST_URL_PATHNAME } export default isOnPlaylistPage diff --git a/src/operations/ui/decrement-number-of-videos-in-playlist.ts b/src/operations/ui/decrement-number-of-videos-in-playlist.ts index d7201e0..a9bb3cc 100644 --- a/src/operations/ui/decrement-number-of-videos-in-playlist.ts +++ b/src/operations/ui/decrement-number-of-videos-in-playlist.ts @@ -12,6 +12,7 @@ export default function decrementNumberOfVideosInPlaylist(value: number) { // - The "There are no videos in this playlist yet" text // - The "No videos" text // Both strings are not part of the `yt.msgs_` object to use for localization + // eslint-disable-next-line no-console console.log('empty playlist reload') window.location.reload() } diff --git a/src/operations/ui/remove-videos-from-playlist.ts b/src/operations/ui/remove-videos-from-playlist.ts index bdebc4e..e2a91e3 100644 --- a/src/operations/ui/remove-videos-from-playlist.ts +++ b/src/operations/ui/remove-videos-from-playlist.ts @@ -17,7 +17,7 @@ function removeVideoWithYtAction(videoId: String) { ], returnValue: [], }, - }) + }), ) } @@ -32,7 +32,7 @@ export default function removeVideosFromPlaylist(videosToDelete: PlaylistVideo[] uniqueVideosToDelete, playlistVideoRendererNodes, (video) => video.videoId, - (node) => node.data.videoId + (node) => node.data.videoId, ) // if all videos to remove are present in the UI if (searchMap) { diff --git a/src/yt-api.ts b/src/yt-api.ts index 173edbd..00815c1 100644 --- a/src/yt-api.ts +++ b/src/yt-api.ts @@ -1,12 +1,10 @@ import sha1 from 'sha1' -import { YTConfigData, PlaylistVideo, Playlist, PlaylistContinuation } from './youtube' import { PlaylistNotEditableError, PlaylistEmptyError } from '~src/errors' +import { YTConfigData, PlaylistVideo, Playlist, PlaylistContinuation } from './youtube' type YTHeaderKey = | 'X-Goog-Visitor-Id' - // eslint-disable-next-line radar/no-duplicate-string | 'X-YouTube-Client-Name' - // eslint-disable-next-line radar/no-duplicate-string | 'X-YouTube-Client-Version' | 'X-YouTube-Device' | 'X-YouTube-Identity-Token' @@ -45,6 +43,7 @@ const API_REQUIRED_HEADERS: HeaderKey[] = [ function generateSAPISIDHASH(origin: string, sapisid: string, date: Date = new Date()): string { const roundedTimestamp = Math.floor(date.getTime() / 1000) // deepcode ignore InsecureHash: we need to replicate youtube webapp behavior + // eslint-disable-next-line sonarjs/no-nested-template-literals return `${roundedTimestamp}_${sha1(`${roundedTimestamp} ${sapisid} ${origin}`)}` } @@ -72,21 +71,19 @@ function generateRequestHeaders(config: YTConfigData, headerKeys: HeaderKey[] = } function extractPlaylistVideoListRendererContents(playlistVideoListContents: Array): PlaylistVideo[] { - return playlistVideoListContents.map( - (item): PlaylistVideo => { - return { - videoId: item.playlistVideoRenderer.videoId, - percentDurationWatched: - item.playlistVideoRenderer.thumbnailOverlays[1].thumbnailOverlayResumePlaybackRenderer - ?.percentDurationWatched || 0, - } + return playlistVideoListContents.map((item): PlaylistVideo => { + return { + videoId: item.playlistVideoRenderer.videoId, + percentDurationWatched: + item.playlistVideoRenderer.thumbnailOverlays[1].thumbnailOverlayResumePlaybackRenderer + ?.percentDurationWatched || 0, } - ) + }) } function extractPlaylistContinuation(playlistContents: Array): PlaylistContinuation { // ContinuationToken should be in the last item of the playlist contents - const lastItem = playlistContents[playlistContents.length - 1] + const lastItem = playlistContents.at(-1) if (lastItem && lastItem.continuationItemRenderer) { // Remove last item from playlist contents since it contain continuationItem playlistContents.pop() @@ -113,8 +110,10 @@ async function fetchPlaylistInitialData(config: YTConfigData, playlistName: stri method: 'GET', mode: 'cors', }) - const data = (await response.json()).response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content - .sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer + const respJson = await response.json() + const data = + respJson.response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer + .contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer if (!data) { throw PlaylistEmptyError @@ -130,7 +129,7 @@ async function fetchPlaylistInitialData(config: YTConfigData, playlistName: stri async function fetchPlaylistContinuation( config: YTConfigData, - continuation: PlaylistContinuation + continuation: PlaylistContinuation, ): Promise { const url = new URL(`${API_GET_PLAYLIST_VIDEOS_URL}`) const headers = generateRequestHeaders(config, API_V1_REQUIRED_HEADERS) @@ -152,7 +151,8 @@ async function fetchPlaylistContinuation( method: 'POST', mode: 'cors', }) - const data = (await response.json()).onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems + const responseJson = await response.json() + const data = responseJson.onResponseReceivedActions[0].appendContinuationItemsAction.continuationItems return extractPlaylistContinuation(data) } @@ -160,11 +160,11 @@ export async function fetchAllPlaylistContent(config: YTConfigData, playlistName const playlist = await fetchPlaylistInitialData(config, playlistName) if (playlist.isEditable) { // If all data has been retrieved, the last continuation item token will be undefined - while (playlist.continuations[playlist.continuations.length - 1].continuationToken !== undefined) { + while (playlist.continuations.at(-1)?.continuationToken !== undefined) { playlist.continuations.push( // We need the next continuationToken to launch the next request // eslint-disable-next-line no-await-in-loop - await fetchPlaylistContinuation(config, playlist.continuations[playlist.continuations.length - 1]) + await fetchPlaylistContinuation(config, playlist.continuations.at(-1)!), ) } // Merge all the videos into a single PlaylistContinuation @@ -193,9 +193,10 @@ async function getRemoveFromHistoryToken(videoId: string): Promise { if (!matchedData || !matchedData[1]) throw new Error('Failed to parse initData') const initData = JSON.parse(matchedData[1]) - const groups = initData?.contents?.twoColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents - .map((group: { itemSectionRenderer: object }) => group.itemSectionRenderer) - .filter(Boolean) + const groups = + initData?.contents?.twoColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents + .map((group: { itemSectionRenderer: object }) => group.itemSectionRenderer) + .filter(Boolean) let matchingVideo for (const item of groups) { @@ -264,7 +265,7 @@ export async function removeWatchHistoryForVideo(config: YTConfigData, videoId: export async function removeVideosFromPlaylist( config: YTConfigData, playlistId: string, - videosToRemove: PlaylistVideo[] + videosToRemove: PlaylistVideo[], ): Promise { const url = new URL(`${API_EDIT_PLAYLIST_VIDEOS_URL}`) const headers = generateRequestHeaders(config, API_V1_REQUIRED_HEADERS) @@ -292,8 +293,5 @@ export async function removeVideosFromPlaylist( mode: 'cors', }) const data = await response.json() - if (data.status !== 'STATUS_SUCCEEDED') { - return true - } - return false + return data.status !== 'STATUS_SUCCEEDED' } diff --git a/test/_setup-browser-environment.js b/test/_setup-browser-environment.js index 436db1d..518e71e 100644 --- a/test/_setup-browser-environment.js +++ b/test/_setup-browser-environment.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line unicorn/prefer-module const browserEnv = require('browser-env') browserEnv() diff --git a/test/metadata.test.ts b/test/metadata.test.ts index a48d5ec..f499a28 100644 --- a/test/metadata.test.ts +++ b/test/metadata.test.ts @@ -20,25 +20,25 @@ test.afterEach(() => { test('generateDownloadUrlFromRepositoryUrl: should extract repo from github ssh url', (t) => { t.is( generateDownloadUrlFromRepositoryUrl('git@github.com:some-user/my-repo.git'), - `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js` + `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js`, ) }) test('generateDownloadUrlFromRepositoryUrl: should extract repo from github https url', (t) => { t.is( generateDownloadUrlFromRepositoryUrl('https://github.com/some-user/my-repo.git'), - `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js` + `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js`, ) }) test('generateDownloadUrlFromRepositoryUrl: should extract repo from git:// url', (t) => { t.is( generateDownloadUrlFromRepositoryUrl('git://github.com/some-user/my-repo.git'), - `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js` + `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js`, ) }) test('generateDownloadUrlFromRepositoryUrl: should extract repo from git+ssh:// url', (t) => { t.is( generateDownloadUrlFromRepositoryUrl('git+ssh://github.com/some-user/my-repo.git'), - `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js` + `https://raw.githubusercontent.com/some-user/my-repo/stubreleaseBranch/stubid.user.js`, ) }) test('generateDownloadUrlFromRepositoryUrl: should return empty string if non-git url', (t) => { diff --git a/test/src/lib/get-elements-by-xpaths.test.tsx b/test/src/lib/get-elements-by-xpaths.test.tsx index 5c4c8a3..92af4b5 100644 --- a/test/src/lib/get-elements-by-xpaths.test.tsx +++ b/test/src/lib/get-elements-by-xpaths.test.tsx @@ -15,7 +15,7 @@ test.beforeEach(() => {
- + , ) container = result.container }) @@ -34,7 +34,7 @@ test.serial('getElementsByXpath: should return array with the matching nodes in
  • One
  • Two
  • Three
  • - + , ) const expected = [ expectedSnap.container.childNodes.item(0), @@ -53,7 +53,7 @@ test.serial('getElementsByXpath: should work without parent element and use docu
  • One
  • Two
  • Three
  • - + , ) const expected = [ expectedSnap.container.childNodes.item(0), diff --git a/test/src/lib/list-map-search.test.ts b/test/src/lib/list-map-search.test.ts index 36efe06..2c679ee 100644 --- a/test/src/lib/list-map-search.test.ts +++ b/test/src/lib/list-map-search.test.ts @@ -25,7 +25,7 @@ test('listMapSearch: should early break when all needles are found', (t) => { { id: 1, value: '41' }, ], needleKeyGetSpy, - haystackKeyGetSpy + haystackKeyGetSpy, ) t.deepEqual(result, { 1: { id: 1, value: '42' } }) t.true(needleKeyGetSpy.calledOnceWith({ id: 1 })) @@ -41,7 +41,7 @@ test('listMapSearch: should early break when all needles are larger than haystac { id: 1, value: '41' }, ], needleKeyGetSpy, - haystackKeyGetSpy + haystackKeyGetSpy, ) t.is(result, false) t.true(needleKeyGetSpy.notCalled) @@ -60,7 +60,7 @@ test('listMapSearch: should not be troubled by duplicates in haystack and keep t { id: 2, value: '69' }, ], getIdAsString, - getIdAsString + getIdAsString, ) t.deepEqual(result, { '1': { id: 1, value: '42' }, '2': { id: 2, value: '69' } }) }) @@ -73,7 +73,7 @@ test('listMapSearch: should return false after all haystack has been tried', (t) { id: 3, value: '69' }, ], getIdAsString, - getIdAsString + getIdAsString, ) t.is(result, false) }) @@ -90,7 +90,7 @@ test('listMapSearch: needle can be plain array', (t) => { { id: 2, value: '69' }, ], (index) => index, - getId + getId, ) t.deepEqual(result, { 1: { id: 1, value: '42' }, 2: { id: 2, value: '69' } }) }) diff --git a/test/src/lib/partition.test.ts b/test/src/lib/partition.test.ts index 07fcc43..b27ec65 100644 --- a/test/src/lib/partition.test.ts +++ b/test/src/lib/partition.test.ts @@ -4,19 +4,19 @@ import partition from '~src/lib/partition' test('partition: should return two array', (t) => { t.deepEqual( partition([], () => true), - [[], []] + [[], []], ) }) test('partition: should put values with truthy predicate to first array', (t) => { t.deepEqual( partition([1, 2, 3], () => true), - [[1, 2, 3], []] + [[1, 2, 3], []], ) }) test('partition: should put values with falsy predicate to second array', (t) => { t.deepEqual( partition([1, 2, 3], () => false), - [[], [1, 2, 3]] + [[], [1, 2, 3]], ) }) test('partition: should properly split array in two according to predicate', (t) => { @@ -25,6 +25,6 @@ test('partition: should properly split array in two according to predicate', (t) [ [4, 5, 6], [1, 2, 3], - ] + ], ) })