Skip to content

Commit

Permalink
Merge branch 'master' into fix-video-removal
Browse files Browse the repository at this point in the history
  • Loading branch information
avallete authored Jul 22, 2024
2 parents 9ea8e5d + 1261110 commit 776bfc9
Show file tree
Hide file tree
Showing 21 changed files with 96 additions and 93 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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'],
Expand All @@ -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],
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ jobs:
npm run coverage:report
env:
NODE_ENV: test
- name: E2E-tests
uses: cypress-io/github-action@v2
with:
build: npm run build
env:
NODE_ENV: test
# - name: E2E-tests
# uses: cypress-io/github-action@v2
# with:
# build: npm run build
# env:
# NODE_ENV: test
# Will update the package.json file with semantic-release and push it to master with a new tag
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ jobs:
npm run coverage:report
env:
NODE_ENV: test
- name: E2E-tests
uses: cypress-io/github-action@v2
with:
build: npm run build
env:
NODE_ENV: test
# - name: E2E-tests
# uses: cypress-io/github-action@v2
# with:
# build: npm run build
# env:
# NODE_ENV: test
- name: Upload coverage to CodeCov
uses: codecov/codecov-action@v1
continue-on-error: true
Expand Down
15 changes: 3 additions & 12 deletions .nycrc
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
## [1.7.1](https://github.com/avallete/yt-playlists-delete-enhancer/compare/v1.7.0...v1.7.1) (2024-05-12)
## [1.7.2](https://github.com/avallete/yt-playlists-delete-enhancer/compare/v1.7.1...v1.7.2) (2024-07-22)


### Bug Fixes

* app initialization ([#272](https://github.com/avallete/yt-playlists-delete-enhancer/issues/272)) ([98b1b5e](https://github.com/avallete/yt-playlists-delete-enhancer/commit/98b1b5ea7cfe9155e40d604cba252314bf64a5cd))
* dependencies and use Node.js 16 ([#261](https://github.com/avallete/yt-playlists-delete-enhancer/issues/261)) ([a06fdd6](https://github.com/avallete/yt-playlists-delete-enhancer/commit/a06fdd642a7b64b7f1b2960680cad920030d9082))
* lintings ([#276](https://github.com/avallete/yt-playlists-delete-enhancer/issues/276)) ([22314c2](https://github.com/avallete/yt-playlists-delete-enhancer/commit/22314c2d966e5d9b9ddcaecb15dfe6cf94d613b9))
* prevent videos from being removed after reset ([#260](https://github.com/avallete/yt-playlists-delete-enhancer/issues/260)) ([7cc139b](https://github.com/avallete/yt-playlists-delete-enhancer/commit/7cc139bb2ab632ab1280dcac6f191b5fcf794c7d))

## [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)

Expand Down
7 changes: 4 additions & 3 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable unicorn/prefer-module */
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
Expand All @@ -12,15 +13,15 @@
// 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')
import { resolve, join } from 'node:path'

const DIST_PATH = path.resolve(path.join(__dirname, '..', '..', 'dist'))
const DIST_PATH = resolve(join(import.meta.dirname, '..', '..', 'dist'))

/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
export default (on, _config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.7.1",
"version": "1.7.2",
"name": "yt-playlists-delete-enhancer",
"description": "Add a button to remove videos watched with more than X percent from watch later playlist.",
"private": true,
Expand Down
19 changes: 10 additions & 9 deletions src/components/remove-video-enhancer-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ export default class RemoveVideoEnhancerApp extends Component<Properties, State>
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] })
}
Expand All @@ -55,7 +59,7 @@ export default class RemoveVideoEnhancerApp extends Component<Properties, State>
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 {
Expand Down Expand Up @@ -88,10 +92,7 @@ export default class RemoveVideoEnhancerApp extends Component<Properties, State>
}

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) {
Expand Down
7 changes: 2 additions & 5 deletions src/components/remove-video-enhancer-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -70,7 +67,7 @@ function RemoveVideoEnhancerContainer({
onClick: removeVideo,
}),
],
element
element,
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/list-map-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default function listMapSearch<T, U, K extends keyof any>(
needles: Array<T>,
haystack: Array<U>,
needleKeyGetter: (item: T) => K,
haystackKeyGetter: (item: U) => K
haystackKeyGetter: (item: U) => K,
): Record<K, U> | false {
const searchMap: Record<K, U | undefined> = {} as Record<K, U>
// We cannot found all our needles into our haystack
Expand Down
2 changes: 2 additions & 0 deletions src/operations/actions/remove-videos-from-playlist-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 1 addition & 4 deletions src/operations/conditions/is-on-playlist-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
4 changes: 2 additions & 2 deletions src/operations/ui/remove-videos-from-playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function removeVideoWithYtAction(videoId: String) {
],
returnValue: [],
},
})
}),
)
}

Expand All @@ -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) {
Expand Down
47 changes: 24 additions & 23 deletions src/yt-api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import sha1 from 'sha1'
import { Innertube } from "youtubei.js";
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'
Expand Down Expand Up @@ -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}`)}`
}

Expand Down Expand Up @@ -72,21 +71,19 @@ function generateRequestHeaders(config: YTConfigData, headerKeys: HeaderKey[] =
}

function extractPlaylistVideoListRendererContents(playlistVideoListContents: Array<any>): 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<any>): 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()
Expand All @@ -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
Expand All @@ -130,7 +129,7 @@ async function fetchPlaylistInitialData(config: YTConfigData, playlistName: stri

async function fetchPlaylistContinuation(
config: YTConfigData,
continuation: PlaylistContinuation
continuation: PlaylistContinuation,
): Promise<PlaylistContinuation> {
const url = new URL(`${API_GET_PLAYLIST_VIDEOS_URL}`)
const headers = generateRequestHeaders(config, API_V1_REQUIRED_HEADERS)
Expand All @@ -152,19 +151,20 @@ 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)
}

export async function fetchAllPlaylistContent(config: YTConfigData, playlistName: string): Promise<Playlist> {
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
Expand Down Expand Up @@ -193,9 +193,10 @@ async function getRemoveFromHistoryToken(videoId: string): Promise<string> {
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) {
Expand Down Expand Up @@ -264,7 +265,7 @@ export async function removeWatchHistoryForVideo(config: YTConfigData, videoId:

export async function removeVideosFromPlaylist(
playlistId: string,
videosToRemove: PlaylistVideo[]
videosToRemove: PlaylistVideo[],
): Promise<boolean> {
const youtube = await Innertube.create({
cookie: document.cookie,
Expand Down
Loading

0 comments on commit 776bfc9

Please sign in to comment.