1
mirror of https://github.com/revanced/revanced-integrations synced 2025-11-21 18:35:37 +01:00

Compare commits

...

81 Commits

Author SHA1 Message Date
semantic-release-bot
cd71c66995 chore(release): 1.14.1-dev.1 [skip ci]
## [1.14.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0...v1.14.1-dev.1) (2024-09-18)

### Bug Fixes

* **YouTube - Check environment:** Only use fields available since Android 8 ([#693](https://github.com/ReVanced/revanced-integrations/issues/693)) ([c0829c3](c0829c3915))
2024-09-18 23:01:16 +00:00
oSumAtrIX
c0829c3915 fix(YouTube - Check environment): Only use fields available since Android 8 (#693) 2024-09-19 00:58:02 +02:00
semantic-release-bot
5e9637e073 chore(release): 1.14.0 [skip ci]
# [1.14.0](https://github.com/ReVanced/revanced-integrations/compare/v1.13.0...v1.14.0) (2024-09-18)

### Bug Fixes

* **YouTube - Check environment patch:** Allow adb installs even if patched more than 30 minutes ago ([5adf8bd](5adf8bdd67))
* **YouTube - Check environment patch:** Show if patched apk is too old, if the install source is not Manager or ADB ([18048f3](18048f3324))
* **YouTube - Check environment patch:** Use app install/update time instead of current time ([#687](https://github.com/ReVanced/revanced-integrations/issues/687)) ([b0d82b0](b0d82b016e))
* **YouTube - GmsCore support:** Show an error toast if GmsCore is included with root mounted installation ([#686](https://github.com/ReVanced/revanced-integrations/issues/686)) ([a4848be](a4848be653))
* **YouTube - Hide layout components:** Hide new kind of community post ([#678](https://github.com/ReVanced/revanced-integrations/issues/678)) ([6be257a](6be257a7a6))
* **YouTube - Return YouTube Dislike:** Show correct value when swiping back to prior Short and disliking ([2eb5e3a](2eb5e3afeb))
* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#684](https://github.com/ReVanced/revanced-integrations/issues/684)) ([27d2b60](27d2b60444))
* **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([0f5dfb4](0f5dfb4e76))
* **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([01f084d](01f084d87a))
* **YouTube:** Fix issues related to playback by replace streaming data ([#680](https://github.com/ReVanced/revanced-integrations/issues/680)) ([0468235](04682353af))

### Features

* Add `Check environment` patch ([#683](https://github.com/ReVanced/revanced-integrations/issues/683)) ([e856455](e856455283))
* **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#691](https://github.com/ReVanced/revanced-integrations/issues/691)) ([6f3d2ff](6f3d2ffb0d))
* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#681](https://github.com/ReVanced/revanced-integrations/issues/681)) ([5314dd9](5314dd90d1))
* **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#679](https://github.com/ReVanced/revanced-integrations/issues/679)) ([2c471f3](2c471f39c2))
* **YouTube:** Add donation link to settings about screen ([#688](https://github.com/ReVanced/revanced-integrations/issues/688)) ([b816c45](b816c45838))
2024-09-18 22:17:16 +00:00
LisoUseInAIKyrios
8e16999420 chore: Merge branch dev to main (#677) 2024-09-18 18:13:56 -04:00
semantic-release-bot
ebfe083a24 chore(release): 1.14.0-dev.12 [skip ci]
# [1.14.0-dev.12](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.11...v1.14.0-dev.12) (2024-09-17)

### Bug Fixes

* **YouTube:** Fix issues related to playback by replace streaming data ([#680](https://github.com/ReVanced/revanced-integrations/issues/680)) ([0468235](04682353af))
2024-09-17 22:48:14 +00:00
Zain
04682353af fix(YouTube): Fix issues related to playback by replace streaming data (#680)
Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-18 00:45:14 +02:00
semantic-release-bot
ca50665ac8 chore(release): 1.14.0-dev.11 [skip ci]
# [1.14.0-dev.11](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.10...v1.14.0-dev.11) (2024-09-17)

### Features

* **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#691](https://github.com/ReVanced/revanced-integrations/issues/691)) ([6f3d2ff](6f3d2ffb0d))
2024-09-17 13:38:01 +00:00
MarcaD
6f3d2ffb0d feat(YouTube - Hide Shorts components): Hide 'Use this sound' button (#691)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-17 15:34:34 +02:00
semantic-release-bot
5ffff1bd40 chore(release): 1.14.0-dev.10 [skip ci]
# [1.14.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.9...v1.14.0-dev.10) (2024-09-11)

### Bug Fixes

* **YouTube - Return YouTube Dislike:** Show correct value when swiping back to prior Short and disliking ([2eb5e3a](2eb5e3afeb))
2024-09-11 03:41:25 +00:00
LisoUseInAIKyrios
2eb5e3afeb fix(YouTube - Return YouTube Dislike): Show correct value when swiping back to prior Short and disliking 2024-09-10 23:37:40 -04:00
semantic-release-bot
8332444c0d chore(release): 1.14.0-dev.9 [skip ci]
# [1.14.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.8...v1.14.0-dev.9) (2024-09-09)

### Bug Fixes

* **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([0f5dfb4](0f5dfb4e76))

### Features

* **YouTube:** Add donation link to settings about screen ([#688](https://github.com/ReVanced/revanced-integrations/issues/688)) ([b816c45](b816c45838))
2024-09-09 07:24:25 +00:00
LisoUseInAIKyrios
b816c45838 feat(YouTube): Add donation link to settings about screen (#688) 2024-09-09 03:21:05 -04:00
LisoUseInAIKyrios
0f5dfb4e76 fix(YouTube - SponsorBlock): Add summary text to 'view my segments' button 2024-09-09 03:17:38 -04:00
semantic-release-bot
2c2641e1cf chore(release): 1.14.0-dev.8 [skip ci]
# [1.14.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.7...v1.14.0-dev.8) (2024-09-06)

### Bug Fixes

* **YouTube - Check environment patch:** Show if patched apk is too old, if the install source is not Manager or ADB ([18048f3](18048f3324))
2024-09-06 09:42:41 +00:00
LisoUseInAIKyrios
18048f3324 fix(YouTube - Check environment patch): Show if patched apk is too old, if the install source is not Manager or ADB 2024-09-06 05:39:33 -04:00
semantic-release-bot
dffe7f6c34 chore(release): 1.14.0-dev.7 [skip ci]
# [1.14.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.6...v1.14.0-dev.7) (2024-09-06)

### Bug Fixes

* **YouTube - Check environment patch:** Allow adb installs even if patched more than 30 minutes ago ([5adf8bd](5adf8bdd67))
2024-09-06 08:58:50 +00:00
LisoUseInAIKyrios
5adf8bdd67 fix(YouTube - Check environment patch): Allow adb installs even if patched more than 30 minutes ago 2024-09-06 04:55:33 -04:00
semantic-release-bot
5f6239b610 chore(release): 1.14.0-dev.6 [skip ci]
# [1.14.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.5...v1.14.0-dev.6) (2024-09-06)

### Bug Fixes

* **YouTube - Check environment patch:** Use app install/update time instead of current time ([#687](https://github.com/ReVanced/revanced-integrations/issues/687)) ([b0d82b0](b0d82b016e))
2024-09-06 08:47:32 +00:00
LisoUseInAIKyrios
b0d82b016e fix(YouTube - Check environment patch): Use app install/update time instead of current time (#687) 2024-09-06 04:44:14 -04:00
semantic-release-bot
5b9e0e8ad4 chore(release): 1.14.0-dev.5 [skip ci]
# [1.14.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.4...v1.14.0-dev.5) (2024-09-06)

### Features

* Add `Check environment` patch ([#683](https://github.com/ReVanced/revanced-integrations/issues/683)) ([e856455](e856455283))
2024-09-06 08:30:14 +00:00
oSumAtrIX
e856455283 feat: Add Check environment patch (#683)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2024-09-06 10:27:02 +02:00
semantic-release-bot
a324b16096 chore(release): 1.14.0-dev.4 [skip ci]
# [1.14.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.3...v1.14.0-dev.4) (2024-09-01)

### Bug Fixes

* **YouTube - GmsCore support:** Show an error toast if GmsCore is included with root mounted installation ([#686](https://github.com/ReVanced/revanced-integrations/issues/686)) ([a4848be](a4848be653))
2024-09-01 22:06:37 +00:00
LisoUseInAIKyrios
a4848be653 fix(YouTube - GmsCore support): Show an error toast if GmsCore is included with root mounted installation (#686)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-01 18:03:43 -04:00
semantic-release-bot
a35dfe8ea3 chore(release): 1.14.0-dev.3 [skip ci]
# [1.14.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.2...v1.14.0-dev.3) (2024-09-01)

### Bug Fixes

* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#684](https://github.com/ReVanced/revanced-integrations/issues/684)) ([27d2b60](27d2b60444))
2024-09-01 21:52:14 +00:00
LisoUseInAIKyrios
27d2b60444 fix(YouTube - ReturnYouTubeDislike): Show estimated like count for videos with hidden likes (#684) 2024-09-01 17:49:15 -04:00
semantic-release-bot
55c278dc08 chore(release): 1.14.0-dev.2 [skip ci]
# [1.14.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.14.0-dev.1...v1.14.0-dev.2) (2024-08-30)

### Features

* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#681](https://github.com/ReVanced/revanced-integrations/issues/681)) ([5314dd9](5314dd90d1))
2024-08-30 21:41:55 +00:00
LisoUseInAIKyrios
5314dd90d1 feat(YouTube - Keyword filter): Add syntax to match whole keywords and not substrings (#681)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-08-30 17:38:44 -04:00
semantic-release-bot
db81332078 chore(release): 1.14.0-dev.1 [skip ci]
# [1.14.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.2...v1.14.0-dev.1) (2024-08-22)

### Features

* **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#679](https://github.com/ReVanced/revanced-integrations/issues/679)) ([2c471f3](2c471f39c2))
2024-08-22 17:50:39 +00:00
LisoUseInAIKyrios
2c471f39c2 feat(YouTube - Spoof client): Allow forcing AVC codec with iOS (#679) 2024-08-22 13:47:15 -04:00
semantic-release-bot
08d9f612a6 chore(release): 1.13.1-dev.2 [skip ci]
## [1.13.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.13.1-dev.1...v1.13.1-dev.2) (2024-08-20)

### Bug Fixes

* **YouTube - Hide layout components:** Hide new kind of community post ([#678](https://github.com/ReVanced/revanced-integrations/issues/678)) ([6be257a](6be257a7a6))
2024-08-20 06:20:04 +00:00
Bceez
6be257a7a6 fix(YouTube - Hide layout components): Hide new kind of community post (#678) 2024-08-20 08:16:43 +02:00
semantic-release-bot
7a58ae5d0e chore(release): 1.13.1-dev.1 [skip ci]
## [1.13.1-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.13.0...v1.13.1-dev.1) (2024-08-20)

### Bug Fixes

* **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([01f084d](01f084d87a))
2024-08-20 04:20:24 +00:00
LisoUseInAIKyrios
01f084d87a fix(YouTube - SponsorBlock): Handle if the user enters an invalid number into any SB settings 2024-08-20 00:16:42 -04:00
semantic-release-bot
9e11ba11d9 chore(release): 1.13.0 [skip ci]
# [1.13.0](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0...v1.13.0) (2024-08-15)

### Features

* **YouTube:** Add `Check watch history domain name resolution` patch ([#675](https://github.com/ReVanced/revanced-integrations/issues/675)) ([57d6834](57d6834a2c))
2024-08-15 17:30:27 +00:00
oSumAtrIX
71233ef0ab chore: Merge branch dev to main (#676) 2024-08-15 21:27:09 +04:00
semantic-release-bot
95bb8a7dea chore(release): 1.13.0-dev.1 [skip ci]
# [1.13.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0...v1.13.0-dev.1) (2024-08-15)

### Features

* **YouTube:** Add `Check watch history domain name resolution` patch ([#675](https://github.com/ReVanced/revanced-integrations/issues/675)) ([57d6834](57d6834a2c))
2024-08-15 16:09:10 +00:00
LisoUseInAIKyrios
57d6834a2c feat(YouTube): Add Check watch history domain name resolution patch (#675) 2024-08-15 12:06:02 -04:00
semantic-release-bot
c8d381de5e chore(release): 1.12.0 [skip ci]
# [1.12.0](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1...v1.12.0) (2024-08-06)

### Bug Fixes

* adjust blacklist ([d8d2a85](d8d2a852d3))
* **YouTube - Alternative thumbnails:** Always use primary thumbnail domain for still captures ([#666](https://github.com/ReVanced/revanced-integrations/issues/666)) ([7cdaf8d](7cdaf8df14))
* **YouTube - Client Spoof:** Restore missing high qualities by spoofing the iOS client user agent  ([#668](https://github.com/ReVanced/revanced-integrations/issues/668)) ([fbf629f](fbf629fd62))
* **YouTube - Disable auto captions:** Do not break Shorts captions menu ([0345a00](0345a00d60))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([cda1f31](cda1f3160c))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([#664](https://github.com/ReVanced/revanced-integrations/issues/664)) ([120188d](120188d643))
* **YouTube - Hide layout components:** Hide new type of horizontal shelf ([1fa59a6](1fa59a62a1))
* **YouTube - Keyword filter:** Filter videos from new subscription layout ([2f2eeea](2f2eeea5a7))
* **YouTube - Return YouTube Dislike:** Fix dislikes not appearing due to new component name ([#674](https://github.com/ReVanced/revanced-integrations/issues/674)) ([509e151](509e1516f8))
* **YouTube - SponsorBlock:** Correctly show minute timestamp when creating a new segment ([e71955d](e71955d5bb))
* **YouTube - SponsorBlock:** Improve create segment manual seek accuracy ([#671](https://github.com/ReVanced/revanced-integrations/issues/671)) ([34c02ae](34c02aeb2a))
* **YouTube - Spoof client:** Fix tracking history on brand accounts ([#669](https://github.com/ReVanced/revanced-integrations/issues/669)) ([4ac698f](4ac698fd4b))
* **YouTube - Spoof client:** Restore livestream audio only playback with iOS spoofing ([#673](https://github.com/ReVanced/revanced-integrations/issues/673)) ([5bf5fbd](5bf5fbd1a7))

### Features

* **YouTube - Description components:** Add  `Hide 'Key concepts' section` option ([#670](https://github.com/ReVanced/revanced-integrations/issues/670)) ([86b25ea](86b25ea468))
* **YouTube:** Add `Bypass image region restrictions` patch ([#667](https://github.com/ReVanced/revanced-integrations/issues/667)) ([396ba77](396ba77c20))
2024-08-06 00:09:03 +00:00
oSumAtrIX
e5736fc27f chore: Merge branch dev to main (#665) 2024-08-06 02:05:46 +02:00
semantic-release-bot
36cb36fbfd chore(release): 1.12.0-dev.10 [skip ci]
# [1.12.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.9...v1.12.0-dev.10) (2024-08-05)

### Bug Fixes

* **YouTube - Return YouTube Dislike:** Fix dislikes not appearing due to new component name ([#674](https://github.com/ReVanced/revanced-integrations/issues/674)) ([509e151](509e1516f8))
2024-08-05 22:11:15 +00:00
oSumAtrIX
509e1516f8 fix(YouTube - Return YouTube Dislike): Fix dislikes not appearing due to new component name (#674) 2024-08-06 00:07:39 +02:00
semantic-release-bot
7017488de9 chore(release): 1.12.0-dev.9 [skip ci]
# [1.12.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.8...v1.12.0-dev.9) (2024-08-04)

### Bug Fixes

* **YouTube - Spoof client:** Restore livestream audio only playback with iOS spoofing ([#673](https://github.com/ReVanced/revanced-integrations/issues/673)) ([5bf5fbd](5bf5fbd1a7))
2024-08-04 19:41:12 +00:00
LisoUseInAIKyrios
5bf5fbd1a7 fix(YouTube - Spoof client): Restore livestream audio only playback with iOS spoofing (#673) 2024-08-04 21:38:07 +02:00
semantic-release-bot
a0f3d7a0f7 chore(release): 1.12.0-dev.8 [skip ci]
# [1.12.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.7...v1.12.0-dev.8) (2024-08-02)

### Bug Fixes

* **YouTube - SponsorBlock:** Improve create segment manual seek accuracy ([#671](https://github.com/ReVanced/revanced-integrations/issues/671)) ([34c02ae](34c02aeb2a))
2024-08-02 13:43:19 +00:00
LisoUseInAIKyrios
34c02aeb2a fix(YouTube - SponsorBlock): Improve create segment manual seek accuracy (#671) 2024-08-02 09:39:33 -04:00
semantic-release-bot
d57a64b659 chore(release): 1.12.0-dev.7 [skip ci]
# [1.12.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.6...v1.12.0-dev.7) (2024-08-01)

### Features

* **YouTube - Description components:** Add  `Hide 'Key concepts' section` option ([#670](https://github.com/ReVanced/revanced-integrations/issues/670)) ([86b25ea](86b25ea468))
2024-08-01 11:30:52 +00:00
ILoveOpenSourceApplications
86b25ea468 feat(YouTube - Description components): Add Hide 'Key concepts' section option (#670)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: ILoveOpenSourceApplications <ILoveOpenSourceApplications@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2024-08-01 13:27:19 +02:00
semantic-release-bot
e879e40e56 chore(release): 1.12.0-dev.6 [skip ci]
# [1.12.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.5...v1.12.0-dev.6) (2024-07-28)

### Bug Fixes

* **YouTube - Keyword filter:** Filter videos from new subscription layout ([2f2eeea](2f2eeea5a7))
2024-07-28 17:22:53 +00:00
LisoUseInAIKyrios
2f2eeea5a7 fix(YouTube - Keyword filter): Filter videos from new subscription layout 2024-07-28 13:19:40 -04:00
semantic-release-bot
3945a37944 chore(release): 1.12.0-dev.5 [skip ci]
# [1.12.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.4...v1.12.0-dev.5) (2024-07-28)

### Bug Fixes

* **YouTube - Client Spoof:** Restore missing high qualities by spoofing the iOS client user agent  ([#668](https://github.com/ReVanced/revanced-integrations/issues/668)) ([fbf629f](fbf629fd62))
2024-07-28 14:54:07 +00:00
Zain Arbani
fbf629fd62 fix(YouTube - Client Spoof): Restore missing high qualities by spoofing the iOS client user agent (#668)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-07-28 16:51:17 +02:00
semantic-release-bot
ab0093ff83 chore(release): 1.12.0-dev.4 [skip ci]
# [1.12.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.3...v1.12.0-dev.4) (2024-07-28)

### Bug Fixes

* **YouTube - Spoof client:** Fix tracking history on brand accounts ([#669](https://github.com/ReVanced/revanced-integrations/issues/669)) ([4ac698f](4ac698fd4b))
2024-07-28 13:56:49 +00:00
oSumAtrIX
4ac698fd4b fix(YouTube - Spoof client): Fix tracking history on brand accounts (#669) 2024-07-28 15:53:55 +02:00
semantic-release-bot
848ed6e878 chore(release): 1.12.0-dev.3 [skip ci]
# [1.12.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.2...v1.12.0-dev.3) (2024-07-26)

### Bug Fixes

* **YouTube - SponsorBlock:** Correctly show minute timestamp when creating a new segment ([e71955d](e71955d5bb))
2024-07-26 14:39:42 +00:00
LisoUseInAIKyrios
e71955d5bb fix(YouTube - SponsorBlock): Correctly show minute timestamp when creating a new segment 2024-07-26 10:36:27 -04:00
semantic-release-bot
7bf43c6896 chore(release): 1.12.0-dev.2 [skip ci]
# [1.12.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.1...v1.12.0-dev.2) (2024-07-15)

### Bug Fixes

* **YouTube - Disable auto captions:** Do not break Shorts captions menu ([0345a00](0345a00d60))
2024-07-15 19:02:18 +00:00
LisoUseInAIKyrios
0345a00d60 fix(YouTube - Disable auto captions): Do not break Shorts captions menu 2024-07-15 18:16:39 +04:00
semantic-release-bot
d996d3832a chore(release): 1.12.0-dev.1 [skip ci]
# [1.12.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.3...v1.12.0-dev.1) (2024-07-14)

### Features

* **YouTube:** Add `Bypass image region restrictions` patch ([#667](https://github.com/ReVanced/revanced-integrations/issues/667)) ([396ba77](396ba77c20))
2024-07-14 22:58:56 +00:00
LisoUseInAIKyrios
396ba77c20 feat(YouTube): Add Bypass image region restrictions patch (#667) 2024-07-15 02:55:42 +04:00
semantic-release-bot
67ff3172bb chore(release): 1.11.2-dev.3 [skip ci]
## [1.11.2-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.2...v1.11.2-dev.3) (2024-07-14)
2024-07-14 22:43:08 +00:00
oSumAtrIX
7af763f4b1 build(Needs bump): Sign APK properly without relying on internal Gradle classes 2024-07-15 00:40:02 +02:00
oSumAtrIX
2fabdb245f ci: Add custom release rule to create a build release 2024-07-15 00:40:02 +02:00
semantic-release-bot
3368023ff9 chore(release): 1.11.2-dev.2 [skip ci]
## [1.11.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.1...v1.11.2-dev.2) (2024-07-14)

### Bug Fixes

* **YouTube - Alternative thumbnails:** Always use primary thumbnail domain for still captures ([#666](https://github.com/ReVanced/revanced-integrations/issues/666)) ([7cdaf8d](7cdaf8df14))
* **YouTube - Hide layout components:** Hide new type of horizontal shelf ([1fa59a6](1fa59a62a1))
2024-07-14 14:15:08 +00:00
LisoUseInAIKyrios
1fa59a62a1 fix(YouTube - Hide layout components): Hide new type of horizontal shelf 2024-07-14 18:11:55 +04:00
semantic-release-bot
66cf6c4263 chore(release): 1.11.2-dev.2 [skip ci]
## [1.11.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.1...v1.11.2-dev.2) (2024-07-13)

### Bug Fixes

* **YouTube - Alternative thumbnails:** Always use primary thumbnail domain for still captures ([#666](https://github.com/ReVanced/revanced-integrations/issues/666)) ([7cdaf8d](7cdaf8df14))
2024-07-13 15:55:01 +00:00
LisoUseInAIKyrios
7cdaf8df14 fix(YouTube - Alternative thumbnails): Always use primary thumbnail domain for still captures (#666) 2024-07-13 19:32:40 +04:00
oSumAtrIX
34ef27de79 ci: Correct usage of repository variable 2024-07-13 00:44:39 +02:00
semantic-release-bot
6fe85a21e9 chore(release): 1.11.2-dev.1 [skip ci]
## [1.11.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1...v1.11.2-dev.1) (2024-07-12)

### Bug Fixes

* adjust blacklist ([d8d2a85](d8d2a852d3))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([cda1f31](cda1f3160c))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([#664](https://github.com/ReVanced/revanced-integrations/issues/664)) ([120188d](120188d643))
2024-07-12 17:52:23 +00:00
LisoUseInAIKyrios
120188d643 fix(YouTube - Hide keyword content): Do not hide flyout menu (#664) 2024-07-12 21:49:18 +04:00
LisoUseInAIKyrios
d8d2a852d3 fix: adjust blacklist 2024-07-12 21:48:36 +04:00
LisoUseInAIKyrios
9469239264 refactor 2024-07-12 21:45:44 +04:00
LisoUseInAIKyrios
cda1f3160c fix(YouTube - Hide keyword content): Do not hide flyout menu 2024-07-12 21:22:42 +04:00
semantic-release-bot
34a224e5de chore(release): 1.11.1 [skip ci]
## [1.11.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.0...v1.11.1) (2024-07-11)

### Bug Fixes

* **YouTube - Hide ads:** Hide new types of home feed button ads ([#662](https://github.com/ReVanced/revanced-integrations/issues/662)) ([ff2637c](ff2637cb4c))
* **YouTube - Hide layout components:** Detect if a keyword filter hides all videos ([#657](https://github.com/ReVanced/revanced-integrations/issues/657)) ([3a3ceec](3a3ceec4b5))
* **YouTube - Hide layout components:** Hide new kind of community post ([#659](https://github.com/ReVanced/revanced-integrations/issues/659)) ([c237e3c](c237e3c02c))
* **YouTube - Settings:** Move some settings to different menus, adjust default setting values ([#661](https://github.com/ReVanced/revanced-integrations/issues/661)) ([77533cf](77533cf3d6))
* **YouTube - SponsorBlock:** Skip segments when casting ([#655](https://github.com/ReVanced/revanced-integrations/issues/655)) ([5ce16ee](5ce16eedc6))
2024-07-11 18:53:43 +00:00
LisoUseInAIKyrios
1aba976b28 chore: Merge branch dev to main (#658) 2024-07-11 22:50:39 +04:00
semantic-release-bot
6fef1b28a4 chore(release): 1.11.1-dev.4 [skip ci]
## [1.11.1-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1-dev.3...v1.11.1-dev.4) (2024-07-10)

### Bug Fixes

* **YouTube - SponsorBlock:** Skip segments when casting ([#655](https://github.com/ReVanced/revanced-integrations/issues/655)) ([5ce16ee](5ce16eedc6))
2024-07-10 22:14:58 +00:00
Sami Alaoui
5ce16eedc6 fix(YouTube - SponsorBlock): Skip segments when casting (#655)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: Hoàng Gia Bảo <70064328+YT-Advanced@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
fix(YouTube - SponsorBlock): Skip segments when casting #655
2024-07-11 00:11:55 +02:00
semantic-release-bot
7aec04647a chore(release): 1.11.1-dev.3 [skip ci]
## [1.11.1-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1-dev.2...v1.11.1-dev.3) (2024-07-05)

### Bug Fixes

* **YouTube - Hide ads:** Hide new types of home feed button ads ([#662](https://github.com/ReVanced/revanced-integrations/issues/662)) ([ff2637c](ff2637cb4c))
* **YouTube - Settings:** Move some settings to different menus, adjust default setting values ([#661](https://github.com/ReVanced/revanced-integrations/issues/661)) ([77533cf](77533cf3d6))
2024-07-05 17:44:15 +00:00
LisoUseInAIKyrios
ff2637cb4c fix(YouTube - Hide ads): Hide new types of home feed button ads (#662) 2024-07-05 21:40:52 +04:00
LisoUseInAIKyrios
77533cf3d6 fix(YouTube - Settings): Move some settings to different menus, adjust default setting values (#661) 2024-07-05 21:38:33 +04:00
semantic-release-bot
d9f7679020 chore(release): 1.11.1-dev.2 [skip ci]
## [1.11.1-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1-dev.1...v1.11.1-dev.2) (2024-06-30)

### Bug Fixes

* **YouTube - Hide layout components:** Hide new kind of community post ([#659](https://github.com/ReVanced/revanced-integrations/issues/659)) ([c237e3c](c237e3c02c))
2024-06-30 18:45:48 +00:00
Bceez
c237e3c02c fix(YouTube - Hide layout components): Hide new kind of community post (#659)
fix(YouTube - Hide layout components) Community posts not hiding
2024-06-30 20:42:52 +02:00
48 changed files with 2578 additions and 1016 deletions

View File

@@ -45,7 +45,7 @@ jobs:
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ env.GPG_FINGERPRINT }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
env:

View File

@@ -7,7 +7,13 @@
}
],
"plugins": [
"@semantic-release/commit-analyzer",
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"gradle-semantic-release-plugin",

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin)
publishing
signing
}
android {
@@ -53,28 +54,27 @@ dependencies {
compileOnly(project(":stub"))
}
tasks {
// Because the signing plugin doesn't support signing APKs, do it manually.
register("sign") {
group = "signing"
dependsOn(build)
tasks {
val assembleReleaseSignApk by registering {
dependsOn("assembleRelease")
val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk")
inputs.file(apk).withPropertyName("input")
outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") })
doLast {
val outputDirectory = layout.buildDirectory.dir("outputs/apk/release").get().asFile
val integrationsApk = outputDirectory.resolve("${rootProject.name}-$version.apk")
org.gradle.security.internal.gnupg.GnupgSignatoryFactory().createSignatory(project).sign(
integrationsApk.inputStream(),
outputDirectory.resolve("${integrationsApk.name}.asc").outputStream(),
)
signing {
useGpgCmd()
sign(*inputs.files.files.toTypedArray())
}
}
}
// Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
publish {
dependsOn(build)
dependsOn("sign")
dependsOn(assembleReleaseSignApk)
}
}

View File

@@ -24,6 +24,7 @@ import java.net.URL;
* @noinspection unused
*/
public class GmsCoreSupport {
public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube";
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
@@ -53,18 +54,15 @@ public class GmsCoreSupport {
String dialogMessageRef,
String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
// Allow using back button to skip the action, just in case the check can never be satisfied.
.setCancelable(true)
.show();
}, 100);
// Do not set cancelable to false, to allow using back button to skip the action,
// just in case the check can never be satisfied.
var dialog = new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
.create();
Utils.showDialog(context, dialog);
}
/**
@@ -73,6 +71,21 @@ public class GmsCoreSupport {
@RequiresApi(api = Build.VERSION_CODES.N)
public static void checkGmsCore(Activity context) {
try {
// Verify the user has not included GmsCore for a root installation.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load
// resources from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// Do not exit. If the app exits before launch completes (and without
// opening another activity), then on some devices such as Pixel phone Android 10
// no toast will be shown and the app will continually be relaunched
// with the appearance of a hung app.
}
// Verify GmsCore is installed.
try {
PackageManager manager = context.getPackageManager();

View File

@@ -1,24 +1,21 @@
package app.revanced.integrations.shared;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_STACKTRACE;
import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
import app.revanced.integrations.shared.settings.BaseSettings;
import static app.revanced.integrations.shared.settings.BaseSettings.*;
public class Logger {
/**
* Log messages using lambdas.
*/
@FunctionalInterface
public interface LogMessage {
@NonNull
String buildMessageString();
@@ -59,19 +56,33 @@ public class Logger {
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message) {
printDebug(message, null);
}
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
var messageString = message.buildMessageString();
String logMessage = message.buildMessageString();
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(messageString);
var builder = new StringBuilder(logMessage);
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
builder.append('\n').append(sw);
messageString = builder.toString();
logMessage = builder.toString();
}
Log.d(REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(), messageString);
if (ex == null) {
Log.d(logTag, logMessage);
} else {
Log.d(logTag, logMessage, ex);
}
}
}

View File

@@ -1,6 +1,10 @@
package app.revanced.integrations.shared;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
@@ -8,6 +12,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
@@ -273,7 +278,6 @@ public class Utils {
@NonNull MatchFilter<View> filter) {
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
Logger.printDebug(() -> "View id: " + childAt.getId() + " tag: " + childAt.getTag());
if (filter.matches(childAt)) {
//noinspection unchecked
@@ -285,6 +289,7 @@ public class Utils {
if (match != null) return match;
}
}
return null;
}
@@ -363,6 +368,99 @@ public class Utils {
return isRightToLeftTextLayout;
}
/**
* @return if the text contains at least 1 number character,
* including any unicode numbers such as Arabic.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean containsNumber(@NonNull CharSequence text) {
for (int index = 0, length = text.length(); index < length;) {
final int codePoint = Character.codePointAt(text, index);
if (Character.isDigit(codePoint)) {
return true;
}
index += Character.charCount(codePoint);
}
return false;
}
/**
* Ignore this class. It must be public to satisfy Android requirement.
*/
@SuppressWarnings("deprecation")
public static class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
@SuppressWarnings("deprecation")
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/**
* Safe to call from any thread
*/

View File

@@ -0,0 +1,164 @@
package app.revanced.integrations.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.DialogFragmentOnStartAction;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.text.Html;
import android.widget.Button;
import androidx.annotation.Nullable;
import java.util.Collection;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
/**
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
*/
@Nullable
protected abstract Boolean check();
protected abstract String failureReason();
/**
* Specifies a sorting order for displaying the checks that failed.
* A lower value indicates to show first before other checks.
*/
public abstract int uiSortingValue();
/**
* For debugging and development only.
* Forces all checks to be performed and the check failed dialog to be shown.
* Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
* set to -1.
*/
static boolean debugAlwaysShowWarning() {
final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
if (alwaysShowWarning) {
Logger.printInfo(() -> "Debug forcing environment check warning to show");
}
return alwaysShowWarning;
}
static boolean shouldRun() {
return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
}
static void disableForever() {
Logger.printInfo(() -> "Environment checks disabled forever");
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();
reasons.append("<ul>");
for (var check : failedChecks) {
// Add a non breaking space to fix bullet points spacing issue.
reasons.append("<li>&nbsp;").append(check.failureReason());
}
reasons.append("</ul>");
var message = Html.fromHtml(
str("revanced_check_environment_failed_message", reasons.toString()),
FROM_HTML_MODE_COMPACT
);
Utils.runOnMainThreadDelayed(() -> {
AlertDialog alert = new AlertDialog.Builder(activity)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("revanced_check_environment_failed_title"))
.setMessage(message)
.setPositiveButton(
" ",
(dialog, which) -> {
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
// Shutdown to prevent the user from navigating back to this app,
// which is no longer showing a warning dialog.
activity.finishAffinity();
System.exit(0);
}
).setNegativeButton(
" ",
(dialog, which) -> {
// Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
dialog.dismiss();
}
).create();
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(AlertDialog dialog) {
// Only run this once, otherwise if the user changes to a different app
// then changes back, this handler will run again and disable the buttons.
if (hasRun) {
return;
}
hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
openWebsiteButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
dismissButton.setEnabled(false);
getCountdownRunnable(dismissButton, openWebsiteButton).run();
}
});
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
}
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@Override
public void run() {
Utils.verifyOnMainThread();
if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
dismissButton.setEnabled(true);
}
}
};
}
}

View File

@@ -0,0 +1,28 @@
package app.revanced.integrations.shared.checks;
// Fields are set by the patch. Do not modify.
// Fields are not final, because the compiler is inlining them.
final class PatchInfo {
static long PATCH_TIME = 0L;
final static class Build {
static String PATCH_BOARD = "";
static String PATCH_BOOTLOADER = "";
static String PATCH_BRAND = "";
static String PATCH_CPU_ABI = "";
static String PATCH_CPU_ABI2 = "";
static String PATCH_DEVICE = "";
static String PATCH_DISPLAY = "";
static String PATCH_FINGERPRINT = "";
static String PATCH_HARDWARE = "";
static String PATCH_HOST = "";
static String PATCH_ID = "";
static String PATCH_MANUFACTURER = "";
static String PATCH_MODEL = "";
static String PATCH_PRODUCT = "";
static String PATCH_RADIO = "";
static String PATCH_TAGS = "";
static String PATCH_TYPE = "";
static String PATCH_USER = "";
}
}

View File

@@ -1,5 +1,6 @@
package app.revanced.integrations.shared.settings.preference;
import static app.revanced.integrations.shared.StringRef.sf;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.requests.Route.Method.GET;
@@ -71,7 +72,7 @@ public class ReVancedAboutPreference extends Preference {
return Color.BLACK;
}
private String createDialogHtml(ReVancedSocialLink[] socialLinks) {
private String createDialogHtml(WebLink[] socialLinks) {
final boolean isNetworkConnected = Utils.isNetworkConnected();
StringBuilder builder = new StringBuilder();
@@ -122,7 +123,7 @@ public class ReVancedAboutPreference extends Preference {
.append("</h2>");
builder.append("<div>");
for (ReVancedSocialLink social : socialLinks) {
for (WebLink social : socialLinks) {
builder.append("<div style=\"margin-bottom: 20px;\">");
builder.append(String.format("<a href=\"%s\">%s</a>", social.url, social.name));
builder.append("</div>");
@@ -151,7 +152,7 @@ public class ReVancedAboutPreference extends Preference {
}
private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) {
ReVancedSocialLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks();
WebLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks();
String htmlDialog = createDialogHtml(socialLinks);
Utils.runOnMainThreadNowOrLater(() -> {
@@ -221,19 +222,19 @@ class WebViewDialog extends Dialog {
}
}
class ReVancedSocialLink {
class WebLink {
final boolean preferred;
final String name;
final String url;
ReVancedSocialLink(JSONObject json) throws JSONException {
WebLink(JSONObject json) throws JSONException {
this(json.getBoolean("preferred"),
json.getString("name"),
json.getString("url")
);
}
ReVancedSocialLink(boolean preferred, String name, String url) {
WebLink(boolean preferred, String name, String url) {
this.preferred = preferred;
this.name = name;
this.url = url;
@@ -251,24 +252,33 @@ class ReVancedSocialLink {
}
class SocialLinksRoutes {
/**
* Simple link to the website donate page,
* rather than fetching and parsing the donation links using the API.
*/
public static final WebLink DONATE_LINK = new WebLink(true,
sf("revanced_settings_about_links_donate").toString(),
"https://revanced.app/donate");
/**
* Links to use if fetch links api call fails.
*/
private static final ReVancedSocialLink[] NO_CONNECTION_STATIC_LINKS = {
new ReVancedSocialLink(true, "ReVanced.app", "https://revanced.app")
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
new WebLink(true, "ReVanced.app", "https://revanced.app"),
DONATE_LINK,
};
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2";
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile();
@Nullable
private static volatile ReVancedSocialLink[] fetchedLinks;
private static volatile WebLink[] fetchedLinks;
static boolean hasFetchedLinks() {
return fetchedLinks != null;
}
static ReVancedSocialLink[] fetchSocialLinks() {
static WebLink[] fetchSocialLinks() {
try {
if (hasFetchedLinks()) return fetchedLinks;
@@ -290,14 +300,17 @@ class SocialLinksRoutes {
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
JSONArray socials = json.getJSONArray("socials");
List<ReVancedSocialLink> links = new ArrayList<>();
List<WebLink> links = new ArrayList<>();
links.add(DONATE_LINK); // Show donate link first.
for (int i = 0, length = socials.length(); i < length; i++) {
ReVancedSocialLink link = new ReVancedSocialLink(socials.getJSONObject(i));
WebLink link = new WebLink(socials.getJSONObject(i));
links.add(link);
}
Logger.printDebug(() -> "links: " + links);
return fetchedLinks = links.toArray(new ReVancedSocialLink[0]);
return fetchedLinks = links.toArray(new WebLink[0]);
} catch (SocketTimeoutException ex) {
Logger.printInfo(() -> "Could not fetch social links", ex); // No toast.

View File

@@ -190,16 +190,17 @@ public final class AlternativeThumbnailsPatch {
* Build the alternative thumbnail url using YouTube provided still video captures.
*
* @param decodedUrl Decoded original thumbnail request url.
* @return The alternative thumbnail url, or the original url. Both without tracking parameters.
* @return The alternative thumbnail url, or if not available NULL.
*/
@NonNull
private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
@Nullable
private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
@NonNull ThumbnailQuality qualityToUse) {
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
return sanitizedReplacement;
}
return decodedUrl.sanitizedUrl;
return null;
}
/**
@@ -284,14 +285,21 @@ public final class AlternativeThumbnailsPatch {
final boolean includeTracking;
if (option.useDeArrow && canUseDeArrowAPI()) {
includeTracking = false; // Do not include view tracking parameters with API call.
final String fallbackUrl = option.useStillImages
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
: decodedUrl.sanitizedUrl;
String fallbackUrl = null;
if (option.useStillImages) {
fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
}
if (fallbackUrl == null) {
fallbackUrl = decodedUrl.sanitizedUrl;
}
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
} else if (option.useStillImages) {
includeTracking = true; // Include view tracking parameters if present.
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
if (sanitizedReplacementUrl == null) {
return originalUrl; // Still capture is not available. Return the untouched original url.
}
} else {
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
}
@@ -345,7 +353,7 @@ public final class AlternativeThumbnailsPatch {
return; // Not a thumbnail.
}
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + url);
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl);
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
if (quality == null) {
@@ -627,14 +635,17 @@ public final class AlternativeThumbnailsPatch {
* YouTube video thumbnail url, decoded into it's relevant parts.
*/
private static class DecodedThumbnailUrl {
/**
* YouTube thumbnail URL prefix. Can be '/vi/' or '/vi_webp/'
*/
private static final String YOUTUBE_THUMBNAIL_PREFIX = "https://i.ytimg.com/vi";
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
@Nullable
static DecodedThumbnailUrl decodeImageUrl(String url) {
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
if (urlPathStartIndex <= 0) return null;
final int urlPathEndIndex = url.indexOf('/', urlPathStartIndex);
if (urlPathEndIndex < 0) return null;
final int videoIdStartIndex = url.indexOf('/', urlPathEndIndex) + 1;
if (videoIdStartIndex <= 0) return null;
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
@@ -647,15 +658,15 @@ public final class AlternativeThumbnailsPatch {
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex,
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
}
final String originalFullUrl;
/** Full usable url, but stripped of any tracking information. */
final String sanitizedUrl;
/** Url up to the video ID. */
final String urlPrefix;
/** Url path, such as 'vi' or 'vi_webp' */
final String urlPath;
final String videoId;
/** Quality, such as hq720 or sddefault. */
final String imageQuality;
@@ -664,11 +675,11 @@ public final class AlternativeThumbnailsPatch {
/** User view tracking parameters, only present on some images. */
final String viewTrackingParameters;
DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
originalFullUrl = fullUrl;
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
urlPrefix = fullUrl.substring(0, videoIdStartIndex);
urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex);
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
@@ -681,9 +692,12 @@ public final class AlternativeThumbnailsPatch {
// Images could be upgraded to webp if they are not already, but this fails quite often,
// especially for new videos uploaded in the last hour.
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
// (as much as 4x slower has been observed, despite the alt webp image being a smaller file).
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
builder.append(urlPrefix);
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
// So always use the primary domain for a higher success rate.
builder.append(YOUTUBE_THUMBNAIL_DOMAIN).append(urlPath).append('/');
builder.append(videoId).append('/');
builder.append(qualityToUse.getAltImageNameToUse());
builder.append('.').append(imageExtension);

View File

@@ -0,0 +1,46 @@
package app.revanced.integrations.youtube.patches;
import static app.revanced.integrations.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS;
import java.util.regex.Pattern;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class BypassImageRegionRestrictionsPatch {
private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get();
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
/**
* YouTube static images domain. Includes user and channel avatar images and community post images.
*/
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
/**
* Injection point. Called off the main thread and by multiple threads at the same time.
*
* @param originalUrl Image url for all image urls loaded.
*/
public static String overrideImageURL(String originalUrl) {
try {
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) {
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
}
return replacement;
}
} catch (Exception ex) {
Logger.printException(() -> "overrideImageURL failure", ex);
}
return originalUrl;
}
}

View File

@@ -0,0 +1,76 @@
package app.revanced.integrations.youtube.patches;
import static app.revanced.integrations.shared.StringRef.str;
import android.app.Activity;
import android.text.Html;
import java.net.InetAddress;
import java.net.UnknownHostException;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class CheckWatchHistoryDomainNameResolutionPatch {
private static final String HISTORY_TRACKING_ENDPOINT = "s.youtube.com";
private static final String SINKHOLE_IPV4 = "0.0.0.0";
private static final String SINKHOLE_IPV6 = "::";
/** @noinspection SameParameterValue */
private static boolean domainResolvesToValidIP(String host) {
try {
InetAddress address = InetAddress.getByName(host);
String hostAddress = address.getHostAddress();
if (address.isLoopbackAddress()) {
Logger.printDebug(() -> host + " resolves to localhost");
} else if (SINKHOLE_IPV4.equals(hostAddress) || SINKHOLE_IPV6.equals(hostAddress)) {
Logger.printDebug(() -> host + " resolves to sinkhole ip");
} else {
return true; // Domain is not blocked.
}
} catch (UnknownHostException e) {
Logger.printDebug(() -> host + " failed to resolve");
}
return false;
}
/**
* Injection point.
*
* Checks if s.youtube.com is blacklisted and playback history will fail to work.
*/
public static void checkDnsResolver(Activity context) {
if (!Utils.isNetworkConnected() || !Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return;
Utils.runOnBackgroundThread(() -> {
try {
if (domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)) {
return;
}
Utils.runOnMainThread(() -> {
var alert = new android.app.AlertDialog.Builder(context)
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dialog.dismiss();
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
dialog.dismiss();
}).create();
Utils.showDialog(context, alert, false, null);
});
} catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver failure", ex);
}
});
}
}

View File

@@ -1,6 +1,7 @@
package app.revanced.integrations.youtube.patches;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class DisableAutoCaptionsPatch {
@@ -11,7 +12,9 @@ public class DisableAutoCaptionsPatch {
public static boolean captionsButtonDisabled;
public static boolean autoCaptionsEnabled() {
return Settings.AUTO_CAPTIONS.get();
return Settings.AUTO_CAPTIONS.get()
// Do not use auto captions for Shorts.
&& !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
}
}

View File

@@ -221,12 +221,11 @@ public class ReturnYouTubeDislikePatch {
String conversionContextString = conversionContext.toString();
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml|")) {
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) {
return original;
}
final CharSequence replacement;
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
// Regular video.
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
@@ -235,46 +234,62 @@ public class ReturnYouTubeDislikePatch {
if (!(original instanceof Spanned)) {
original = new SpannableString(original);
}
replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original,
return videoData.getDislikesSpanForRegularVideo((Spanned) original,
true, isRollingNumber);
} else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) {
// Litho Shorts player.
if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) {
// Must clear the current video here, otherwise if the user opens a regular video
// then opens a litho short (while keeping the regular video on screen), then closes the short,
// the original video may show the incorrect dislike value.
clearData();
return original;
}
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
if (videoData == null) {
// The Shorts litho video id filter did not detect the video id.
// This is normal in incognito mode, but otherwise is abnormal.
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
return original;
}
// Use the correct dislikes data after voting.
if (lithoShortsShouldUseCurrentData) {
lithoShortsShouldUseCurrentData = false;
videoData = currentVideoData;
if (videoData == null) {
Logger.printException(() -> "currentVideoData is null"); // Should never happen
return original;
}
Logger.printDebug(() -> "Using current video data for litho span");
}
replacement = videoData.getDislikeSpanForShort((Spanned) original);
} else {
return original;
}
return replacement;
if (isRollingNumber) {
return original; // No need to check for Shorts in the context.
}
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
return getShortsSpan(original, true);
}
if (conversionContextString.contains("|shorts_like_button.eml")
&& !Utils.containsNumber(original)) {
Logger.printDebug(() -> "Replacing hidden likes count");
return getShortsSpan(original, false);
}
} catch (Exception ex) {
Logger.printException(() -> "onLithoTextLoaded failure", ex);
}
return original;
}
private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) {
// Litho Shorts player.
if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get())
|| (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) {
return original;
}
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
if (videoData == null) {
// The Shorts litho video id filter did not detect the video id.
// This is normal in incognito mode, but otherwise is abnormal.
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
return original;
}
// Use the correct dislikes data after voting.
if (lithoShortsShouldUseCurrentData) {
if (isDislikesSpan) {
lithoShortsShouldUseCurrentData = false;
}
videoData = currentVideoData;
if (videoData == null) {
Logger.printException(() -> "currentVideoData is null"); // Should never happen
return original;
}
Logger.printDebug(() -> "Using current video data for litho span");
}
return isDislikesSpan
? videoData.getDislikeSpanForShort((Spanned) original)
: videoData.getLikeSpanForShort((Spanned) original);
}
//
// Rolling Number
//
@@ -597,6 +612,7 @@ public class ReturnYouTubeDislikePatch {
Logger.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
fetch.getFetchData(20000); // Any arbitrarily large max wait time.
}
// Set the fields after the fetch completes, so any concurrent calls will also wait.
lastPlayerResponseWasShort = videoIdIsShort;
lastPrefetchedVideoId = videoId;
@@ -648,6 +664,7 @@ public class ReturnYouTubeDislikePatch {
if (videoIdIsSame(lastLithoShortsVideoData, videoId)) {
return;
}
if (videoId == null) {
// Litho filter did not detect the video id. App is in incognito mode,
// or the proto buffer structure was changed and the video id is no longer present.
@@ -657,6 +674,7 @@ public class ReturnYouTubeDislikePatch {
clearData();
return;
}
Logger.printDebug(() -> "New litho Shorts video id: " + videoId);
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
videoData.setVideoIdIsShort(true);

View File

@@ -7,7 +7,6 @@ import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Objects;
/**
@@ -15,15 +14,21 @@ import java.util.Objects;
* @noinspection unused
*/
public final class VideoInformation {
public interface PlaybackController {
// Methods are added to YT classes during patching.
boolean seekTo(long videoTime);
boolean seekToRelative(long videoTimeOffset);
}
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
private static final String SEEK_METHOD_NAME = "seekTo";
/**
* Prefix present in all Short player parameters signature.
*/
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
private static WeakReference<Object> playerControllerRef;
private static Method seekMethod;
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
@NonNull
private static String videoId = "";
@@ -45,20 +50,30 @@ public final class VideoInformation {
*
* @param playerController player controller object.
*/
public static void initialize(@NonNull Object playerController) {
public static void initialize(@NonNull PlaybackController playerController) {
try {
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
videoTime = -1;
videoLength = 0;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
seekMethod = playerController.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
seekMethod.setAccessible(true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize", ex);
}
}
/**
* Injection point.
*
* @param mdxPlayerDirector MDX player director object (casting mode).
*/
public static void initializeMdx(@NonNull PlaybackController mdxPlayerDirector) {
try {
mdxPlayerDirectorRef = new WeakReference<>(Objects.requireNonNull(mdxPlayerDirector));
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize MDX", ex);
}
}
/**
* Injection point.
*
@@ -177,18 +192,80 @@ public final class VideoInformation {
return false;
}
Logger.printDebug(() -> "Seeking to " + adjustedSeekTime);
//noinspection DataFlowIssue
return (Boolean) seekMethod.invoke(playerControllerRef.get(), adjustedSeekTime);
Logger.printDebug(() -> "Seeking to: " + adjustedSeekTime);
// Try regular playback controller first, and it will not succeed if casting.
PlaybackController controller = playerControllerRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
} else {
if (controller.seekTo(adjustedSeekTime)) return true;
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
// Else the video is loading or changing videos, or video is casting to a different device.
}
// Try calling the seekTo method of the MDX player director (called when casting).
// The difference has to be a different second mark in order to avoid infinite skip loops
// as the Lounge API only supports seconds.
if (adjustedSeekTime / 1000 == videoTime / 1000) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
return false;
}
controller = mdxPlayerDirectorRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo MXD because player controller is null");
return false;
}
return controller.seekTo(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek", ex);
return false;
}
}
/** @noinspection UnusedReturnValue*/
public static boolean seekToRelative(long millisecondsRelative) {
return seekTo(videoTime + millisecondsRelative);
/**
* Seeks a relative amount. Should always be used over {@link #seekTo(long)}
* when the desired seek time is an offset of the current time.
*
* @noinspection UnusedReturnValue
*/
public static boolean seekToRelative(long seekTime) {
Utils.verifyOnMainThread();
try {
Logger.printDebug(() -> "Seeking relative to: " + seekTime);
// Try regular playback controller first, and it will not succeed if casting.
PlaybackController controller = playerControllerRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
} else {
if (controller.seekToRelative(seekTime)) return true;
Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD.");
}
// Adjust the fine adjustment function so it's at least 1 second before/after.
// Otherwise the fine adjustment will do nothing when casting.
final long adjustedSeekTime;
if (seekTime < 0) {
adjustedSeekTime = Math.min(seekTime, -1000);
} else {
adjustedSeekTime = Math.max(seekTime, 1000);
}
controller = mdxPlayerDirectorRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
return false;
}
return controller.seekToRelative(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek relative", ex);
return false;
}
}
/**

View File

@@ -1,6 +1,7 @@
package app.revanced.integrations.youtube.patches.announcements;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Build;
import android.text.Html;
import android.text.method.LinkMovementMethod;
@@ -103,8 +104,6 @@ public final class AnnouncementsPatch {
// Do not show the announcement, if the last announcement id is the same as the current one.
if (Settings.ANNOUNCEMENT_LAST_ID.get() == id) return;
int finalId = id;
final var finalTitle = title;
final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT);
@@ -112,7 +111,7 @@ public final class AnnouncementsPatch {
Utils.runOnMainThread(() -> {
// Show the announcement.
var alertDialog = new android.app.AlertDialog.Builder(context)
var alert = new AlertDialog.Builder(context)
.setTitle(finalTitle)
.setMessage(finalMessage)
.setIcon(finalLevel.icon)
@@ -123,11 +122,13 @@ public final class AnnouncementsPatch {
dialog.dismiss();
})
.setCancelable(false)
.show();
.create();
// Make links clickable.
((TextView)alertDialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
// Make links clickable.
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
});
} catch (Exception e) {
final var message = "Failed to get announcement";

View File

@@ -56,10 +56,11 @@ public final class AdsFilter extends Filter {
final var buttonedAd = new StringFilterGroup(
Settings.HIDE_BUTTONED_ADS,
"_buttoned_layout",
"full_width_square_image_layout",
"_ad_with",
"text_image_button_group_layout",
"_buttoned_layout",
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
"image_button_group_layout",
"full_width_square_image_layout",
"video_display_button_group_layout",
"landscape_image_wide_button_layout",
"video_display_carousel_button_group_layout"

View File

@@ -1,14 +1,19 @@
package app.revanced.integrations.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
final class DescriptionComponentsFilter extends Filter {
private final StringTrieSearch exceptions = new StringTrieSearch();
private final ByteArrayFilterGroupList macroMarkersCarouselGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup macroMarkersCarousel;
public DescriptionComponentsFilter() {
exceptions.addPatterns(
"compact_channel",
@@ -25,11 +30,6 @@ final class DescriptionComponentsFilter extends Filter {
"video_attributes_section"
);
final StringFilterGroup chaptersSection = new StringFilterGroup(
Settings.HIDE_CHAPTERS_SECTION,
"macro_markers_carousel"
);
final StringFilterGroup infoCardsSection = new StringFilterGroup(
Settings.HIDE_INFO_CARDS_SECTION,
"infocards_section"
@@ -45,21 +45,44 @@ final class DescriptionComponentsFilter extends Filter {
"transcript_section"
);
macroMarkersCarousel = new StringFilterGroup(
null,
"macro_markers_carousel.eml"
);
macroMarkersCarouselGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_CHAPTERS_SECTION,
"chapters_horizontal_shelf"
),
new ByteArrayFilterGroup(
Settings.HIDE_KEY_CONCEPTS_SECTION,
"learning_concept_macro_markers_carousel_shelf"
)
);
addPathCallbacks(
attributesSection,
chaptersSection,
infoCardsSection,
podcastSection,
transcriptSection
transcriptSection,
macroMarkersCarousel
);
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (exceptions.matches(path)) return false;
if (matchedGroup == macroMarkersCarousel) {
if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
}

View File

@@ -80,7 +80,9 @@ public final class LayoutComponentsFilter extends Filter {
final var communityPosts = new StringFilterGroup(
Settings.HIDE_COMMUNITY_POSTS,
"post_base_wrapper",
"image_post_root.eml"
"image_post_root.eml",
"text_post_root.eml",
"images_post_root.eml"
);
final var communityGuidelines = new StringFilterGroup(
@@ -163,11 +165,6 @@ public final class LayoutComponentsFilter extends Filter {
"inline_expander"
);
final var videoQualityMenuFooter = new StringFilterGroup(
Settings.HIDE_VIDEO_QUALITY_MENU_FOOTER,
"quality_sheet_footer"
);
final var channelBar = new StringFilterGroup(
Settings.HIDE_CHANNEL_BAR,
"channel_bar"
@@ -254,6 +251,7 @@ public final class LayoutComponentsFilter extends Filter {
Settings.HIDE_HORIZONTAL_SHELVES,
"horizontal_video_shelf.eml",
"horizontal_shelf.eml",
"horizontal_shelf_inline.eml",
"horizontal_tile_shelf.eml"
);
@@ -275,7 +273,6 @@ public final class LayoutComponentsFilter extends Filter {
compactBanner,
compactChannelBarInner,
medicalPanel,
videoQualityMenuFooter,
infoPanel,
emergencyBox,
subscribersCommunityGuidelines,

View File

@@ -14,6 +14,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroup exception;
private final StringFilterGroup videoQualityMenuFooter;
@RequiresApi(api = Build.VERSION_CODES.N)
public PlayerFlyoutMenuItemsFilter() {
@@ -23,8 +24,13 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
"quality_sheet"
);
// Using pathFilterGroupList due to new flyout panel(A/B)
videoQualityMenuFooter = new StringFilterGroup(
Settings.HIDE_VIDEO_QUALITY_MENU_FOOTER,
"quality_sheet_footer"
);
addPathCallbacks(
videoQualityMenuFooter,
new StringFilterGroup(null, "overflow_menu_item.eml|")
);
@@ -75,7 +81,10 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// Only 1 path callback was added, so the matched group must be the overflow menu.
if (matchedGroup == videoQualityMenuFooter) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
if (contentIndex != 0) {
return false; // Overflow menu is always the start of the path.
}

View File

@@ -52,7 +52,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
@SuppressWarnings("unused")
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
try {
if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return;
}
synchronized (lastVideoIds) {
@@ -68,14 +68,22 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
public ReturnYouTubeDislikeFilterPatch() {
// When a new Short is opened, the like buttons always seem to load before the dislike.
// But if swiping back to a previous video and liking/disliking, then only that single button reloads.
// So must check for both buttons.
addPathCallbacks(
new StringFilterGroup(Settings.RYD_SHORTS, "|shorts_dislike_button.eml|")
new StringFilterGroup(null, "|shorts_like_button.eml"),
new StringFilterGroup(null, "|shorts_dislike_button.eml")
);
// After the dislikes icon name is some binary data and then the video id for that specific short.
// After the likes icon name is some binary data and then the video id for that specific short.
videoIdFilterGroup.addAll(
// Video was previously disliked before video was opened.
// on_shadowed = Video was previously like/disliked before opening.
// off_shadowed = Video was not previously liked/disliked before opening.
new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"),
new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed"),
new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
// Video was not already disliked.
new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
);
}
@@ -83,6 +91,10 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return false;
}
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(protobufBufferArray);
@@ -104,6 +116,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
return videoId;
}
}
return null;
}
}
@@ -125,6 +138,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
return true;
}
}
return false;
}
}

View File

@@ -202,6 +202,10 @@ public final class ShortsFilter extends Filter {
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON,
"yt_outline_dollar_sign_heart_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON,
"yt_outline_camera_"
)
);
}

View File

@@ -1,19 +1,20 @@
package app.revanced.integrations.youtube.patches.playback.quality;
import androidx.annotation.Nullable;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.NetworkType;
import app.revanced.integrations.shared.settings.IntegerSetting;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.NetworkType;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.IntegerSetting;
import app.revanced.integrations.youtube.patches.VideoInformation;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
@@ -158,7 +159,7 @@ public class RememberVideoQualityPatch {
/**
* Injection point.
*/
public static void newVideoStarted(Object ignoredPlayerController) {
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Logger.printDebug(() -> "newVideoStarted");
qualityNeedsUpdating = true;
videoQualities = null;

View File

@@ -13,7 +13,7 @@ public final class RememberPlaybackSpeedPatch {
/**
* Injection point.
*/
public static void newVideoStarted(Object ignoredPlayerController) {
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Logger.printDebug(() -> "newVideoStarted");
VideoInformation.overridePlaybackSpeed(Settings.PLAYBACK_SPEED_DEFAULT.get());
}

View File

@@ -0,0 +1,79 @@
package app.revanced.integrations.youtube.patches.spoof;
import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowAV1;
import static app.revanced.integrations.youtube.patches.spoof.DeviceHardwareSupport.allowVP9;
import android.os.Build;
import androidx.annotation.Nullable;
public enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
IOS(5,
// iPhone 15 supports AV1 hardware decoding.
// Only use if this Android device also has hardware decoding.
allowAV1()
? "iPhone16,2" // 15 Pro Max
: "iPhone11,4", // XS Max
// iOS 14+ forces VP9.
allowVP9()
? "17.5.1.21F90"
: "13.7.17H35",
allowVP9()
? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
: "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
null,
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
"19.10.7"
),
ANDROID_VR(28,
"Quest 3",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
"32", // Android 12.1
"1.56.21"
);
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
public final int id;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
public final String model;
/**
* Device OS version.
*/
public final String osVersion;
/**
* Player user-agent.
*/
public final String userAgent;
/**
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
* Field is null if not applicable.
*/
@Nullable
public final String androidSdkVersion;
/**
* App version.
*/
public final String appVersion;
ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) {
this.id = id;
this.model = model;
this.osVersion = osVersion;
this.userAgent = userAgent;
this.androidSdkVersion = androidSdkVersion;
this.appVersion = appVersion;
}
}

View File

@@ -0,0 +1,53 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.os.Build;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
public class DeviceHardwareSupport {
public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
public static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1;
static {
boolean vp9found = false;
boolean av1found = false;
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
final boolean deviceIsAndroidTenOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater
? codecInfo.isHardwareAccelerated()
: !codecInfo.getName().startsWith("OMX.google"); // Software decoder.
if (isHardwareAccelerated && !codecInfo.isEncoder()) {
for (String type : codecInfo.getSupportedTypes()) {
if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) {
vp9found = true;
} else if (type.equalsIgnoreCase("video/av01")) {
av1found = true;
}
}
}
}
DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found;
DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found;
Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1
? "Device supports AV1 hardware decoding\n"
: "Device does not support AV1 hardware decoding\n"
+ (DEVICE_HAS_HARDWARE_DECODING_VP9
? "Device supports VP9 hardware decoding"
: "Device does not support VP9 hardware decoding"));
}
public static boolean allowVP9() {
return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC.get();
}
public static boolean allowAV1() {
return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1;
}
}

View File

@@ -1,188 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.net.Uri;
import android.os.Build;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofClientPatch {
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR;
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_CLIENT_ENABLED) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
*
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_CLIENT_ENABLED) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
public static int getClientTypeId(int originalClientTypeId) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.id;
}
return originalClientTypeId;
}
/**
* Injection point.
*/
public static String getClientVersion(String originalClientVersion) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.version;
}
return originalClientVersion;
}
/**
* Injection point.
*/
public static String getClientModel(String originalClientModel) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.model;
}
return originalClientModel;
}
/**
* Injection point.
*/
public static boolean enablePlayerGesture(boolean original) {
return SPOOF_CLIENT_ENABLED || original;
}
/**
* Injection point.
*/
public static boolean isClientSpoofingEnabled() {
return SPOOF_CLIENT_ENABLED;
}
/**
* Injection point.
* When spoofing the client to iOS, the playback speed menu is missing from the player response.
* Return true to force create the playback speed menu.
*/
public static boolean forceCreatePlaybackSpeedMenu(boolean original) {
if (SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS) {
return true;
}
return original;
}
private enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
ANDROID_VR(28, "Quest 3", "1.56.21"),
// 11,4 = iPhone XS Max.
// 16,2 = iPhone 15 Pro Max.
// Since the 15 supports AV1 hardware decoding, only spoof that device if this
// Android device also has hardware decoding.
//
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
IOS(5, deviceHasAV1HardwareDecoding() ? "iPhone16,2" : "iPhone11,4", "19.10.7");
/**
* YouTube
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
*/
final int id;
/**
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
*/
final String model;
/**
* App version.
*/
final String version;
ClientType(int id, String model, String version) {
this.id = id;
this.model = model;
this.version = version;
}
}
private static boolean deviceHasAV1HardwareDecoding() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) {
if (codecInfo.isHardwareAccelerated() && !codecInfo.isEncoder()) {
String[] supportedTypes = codecInfo.getSupportedTypes();
for (String type : supportedTypes) {
if (type.equalsIgnoreCase("video/av01")) {
MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(type);
if (capabilities != null) {
Logger.printDebug(() -> "Device supports AV1 hardware decoding.");
return true;
}
}
}
}
}
}
Logger.printDebug(() -> "Device does not support AV1 hardware decoding.");
return false;
}
}

View File

@@ -1,242 +0,0 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.patches.VideoInformation;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static app.revanced.integrations.shared.Utils.containsAny;
import static app.revanced.integrations.youtube.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
/** @noinspection unused*/
@Deprecated
public class SpoofSignaturePatch {
/**
* Parameter (also used by
* <a href="https://github.com/yt-dlp/yt-dlp/blob/81ca451480051d7ce1a31c017e005358345a9149/yt_dlp/extractor/youtube.py#L3602">yt-dlp</a>)
* to fix playback issues.
*/
private static final String INCOGNITO_PARAMETERS = "CgIQBg==";
/**
* Parameters used when playing clips.
*/
private static final String CLIPS_PARAMETERS = "kAIB";
/**
* Parameters causing playback issues.
*/
private static final String[] AUTOPLAY_PARAMETERS = {
"YAHI", // Autoplay in feed.
"SAFg" // Autoplay in scrim.
};
/**
* Parameter used for autoplay in scrim.
* Prepend this parameter to mute video playback (for autoplay in feed).
*/
private static final String SCRIM_PARAMETER = "SAFgAXgB";
/**
* Last video id loaded. Used to prevent reloading the same spec multiple times.
*/
@Nullable
private static volatile String lastPlayerResponseVideoId;
@Nullable
private static volatile Future<StoryboardRenderer> rendererFuture;
private static volatile boolean useOriginalStoryboardRenderer;
private static volatile boolean isPlayingShorts;
@Nullable
private static StoryboardRenderer getRenderer(boolean waitForCompletion) {
Future<StoryboardRenderer> future = rendererFuture;
if (future != null) {
try {
if (waitForCompletion || future.isDone()) {
return future.get(20000, TimeUnit.MILLISECONDS); // Any arbitrarily large timeout.
} // else, return null.
} catch (TimeoutException ex) {
Logger.printDebug(() -> "Could not get renderer (get timed out)");
} catch (ExecutionException | InterruptedException ex) {
// Should never happen.
Logger.printException(() -> "Could not get renderer", ex);
}
}
return null;
}
/**
* Injection point.
*
* Called off the main thread, and called multiple times for each video.
*
* @param parameters Original protobuf parameter value.
*/
public static String spoofParameter(String parameters, String videoId, boolean isShortAndOpeningOrPlaying) {
try {
Logger.printDebug(() -> "Original protobuf parameter value: " + parameters);
if (parameters == null || !Settings.SPOOF_SIGNATURE.get()) {
return parameters;
}
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
// Clips are 60 seconds or less in length, so no spoofing.
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = parameters.length() > 150 || parameters.startsWith(CLIPS_PARAMETERS)) {
return parameters;
}
// Shorts do not need to be spoofed.
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = VideoInformation.playerParametersAreShort(parameters)) {
isPlayingShorts = true;
return parameters;
}
isPlayingShorts = false;
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
if (isPlayingFeed) {
//noinspection AssignmentUsedAsCondition
if (useOriginalStoryboardRenderer = !Settings.SPOOF_SIGNATURE_IN_FEED.get()) {
// Don't spoof the feed video playback. This will cause video playback issues,
// but only if user continues watching for more than 1 minute.
return parameters;
}
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
fetchStoryboardRenderer();
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
}
fetchStoryboardRenderer();
} catch (Exception ex) {
Logger.printException(() -> "spoofParameter failure", ex);
}
return INCOGNITO_PARAMETERS;
}
private static void fetchStoryboardRenderer() {
if (!Settings.SPOOF_STORYBOARD_RENDERER.get()) {
lastPlayerResponseVideoId = null;
rendererFuture = null;
return;
}
String videoId = VideoInformation.getPlayerResponseVideoId();
if (!videoId.equals(lastPlayerResponseVideoId)) {
rendererFuture = Utils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
lastPlayerResponseVideoId = videoId;
}
// Block until the renderer fetch completes.
// This is desired because if this returns without finishing the fetch
// then video will start playback but the storyboard is not ready yet.
getRenderer(true);
}
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
boolean returnNullIfLiveStream) {
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (returnNullIfLiveStream && renderer.isLiveStream) {
return null;
}
if (renderer.spec != null) {
return renderer.spec;
}
}
}
return originalStoryboardRendererSpec;
}
/**
* Injection point.
* Called from background threads and from the main thread.
*/
@Nullable
public static String getStoryboardRendererSpec(String originalStoryboardRendererSpec) {
return getStoryboardRendererSpec(originalStoryboardRendererSpec, false);
}
/**
* Injection point.
* Uses additional check to handle live streams.
* Called from background threads and from the main thread.
*/
@Nullable
public static String getStoryboardDecoderRendererSpec(String originalStoryboardRendererSpec) {
return getStoryboardRendererSpec(originalStoryboardRendererSpec, true);
}
/**
* Injection point.
*/
public static int getRecommendedLevel(int originalLevel) {
if (Settings.SPOOF_SIGNATURE.get() && !useOriginalStoryboardRenderer) {
StoryboardRenderer renderer = getRenderer(false);
if (renderer != null) {
if (renderer.recommendedLevel != null) {
return renderer.recommendedLevel;
}
}
}
return originalLevel;
}
/**
* Injection point. Forces seekbar to be shown for paid videos or
* if {@link Settings#SPOOF_STORYBOARD_RENDERER} is not enabled.
*/
public static boolean getSeekbarThumbnailOverrideValue() {
if (!Settings.SPOOF_SIGNATURE.get()) {
return false;
}
StoryboardRenderer renderer = getRenderer(false);
if (renderer == null) {
// Spoof storyboard renderer is turned off,
// video is paid, or the storyboard fetch timed out.
// Show empty thumbnails so the seek time and chapters still show up.
return true;
}
return renderer.spec != null;
}
/**
* Injection point.
*
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
*/
public static void seekbarImageViewCreated(ImageView view) {
try {
if (!Settings.SPOOF_SIGNATURE.get()
|| Settings.SPOOF_STORYBOARD_RENDERER.get()) {
return;
}
if (isPlayingShorts) return;
view.setVisibility(View.GONE);
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
ViewGroup parentLayout = (ViewGroup) view.getParent();
parentLayout.setPadding(0, 0, 0, 0);
} catch (Exception ex) {
Logger.printException(() -> "seekbarImageViewCreated failure", ex);
}
}
}

View File

@@ -0,0 +1,170 @@
package app.revanced.integrations.youtube.patches.spoof;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.Objects;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.patches.spoof.requests.StreamingDataRequest;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class SpoofVideoStreamsPatch {
public static final class ForceiOSAVCAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return Settings.SPOOF_VIDEO_STREAMS.get() && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS;
}
}
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_VIDEO_STREAMS.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_STREAMING_DATA) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by returning unreachable url");
return UNREACHABLE_HOST_URI_STRING;
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
public static boolean isSpoofingEnabled() {
return SPOOF_STREAMING_DATA;
}
/**
* Injection point.
*/
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
if (SPOOF_STREAMING_DATA) {
try {
Uri uri = Uri.parse(url);
String path = uri.getPath();
// 'heartbeat' has no video id and appears to be only after playback has started.
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
}
} catch (Exception ex) {
Logger.printException(() -> "buildRequest failure", ex);
}
}
}
/**
* Injection point.
* Fix playback by replace the streaming data.
* Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static ByteBuffer getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);
if (request != null) {
// This hook is always called off the main thread,
// but this can later be called for the same video id from the main thread.
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
var stream = request.getStream();
if (stream != null) {
Logger.printDebug(() -> "Overriding video stream: " + videoId);
return stream;
}
}
Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId);
} catch (Exception ex) {
Logger.printException(() -> "getStreamingData failure", ex);
}
}
return null;
}
/**
* Injection point.
* Called after {@link #getStreamingData(String)}.
*/
@Nullable
public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) {
if (SPOOF_STREAMING_DATA) {
try {
final int methodPost = 2;
if (method == methodPost) {
String path = uri.getPath();
String clientNameQueryKey = "c";
final boolean iosClient = "IOS".equals(uri.getQueryParameter(clientNameQueryKey));
if (iosClient && path != null && path.contains("videoplayback")) {
return null;
}
}
} catch (Exception ex) {
Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex);
}
}
return postData;
}
}

Some files were not shown because too many files have changed in this diff Show More