test_postprocessors.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. #!/usr/bin/env python3
  2. # Allow direct execution
  3. import os
  4. import sys
  5. import unittest
  6. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  7. import subprocess
  8. from yt_dlp import YoutubeDL
  9. from yt_dlp.utils import shell_quote
  10. from yt_dlp.postprocessor import (
  11. ExecPP,
  12. FFmpegThumbnailsConvertorPP,
  13. MetadataFromFieldPP,
  14. MetadataParserPP,
  15. ModifyChaptersPP,
  16. SponsorBlockPP,
  17. )
  18. class TestMetadataFromField(unittest.TestCase):
  19. def test_format_to_regex(self):
  20. self.assertEqual(
  21. MetadataParserPP.format_to_regex('%(title)s - %(artist)s'),
  22. r'(?P<title>.+)\ \-\ (?P<artist>.+)')
  23. self.assertEqual(MetadataParserPP.format_to_regex(r'(?P<x>.+)'), r'(?P<x>.+)')
  24. def test_field_to_template(self):
  25. self.assertEqual(MetadataParserPP.field_to_template('title'), '%(title)s')
  26. self.assertEqual(MetadataParserPP.field_to_template('1'), '1')
  27. self.assertEqual(MetadataParserPP.field_to_template('foo bar'), 'foo bar')
  28. self.assertEqual(MetadataParserPP.field_to_template(' literal'), ' literal')
  29. def test_metadatafromfield(self):
  30. self.assertEqual(
  31. MetadataFromFieldPP.to_action('%(title)s \\: %(artist)s:%(title)s : %(artist)s'),
  32. (MetadataParserPP.Actions.INTERPRET, '%(title)s : %(artist)s', '%(title)s : %(artist)s'))
  33. class TestConvertThumbnail(unittest.TestCase):
  34. def test_escaping(self):
  35. pp = FFmpegThumbnailsConvertorPP()
  36. if not pp.available:
  37. print('Skipping: ffmpeg not found')
  38. return
  39. test_data_dir = 'test/testdata/thumbnails'
  40. generated_file = f'{test_data_dir}/empty.webp'
  41. subprocess.check_call([
  42. pp.executable, '-y', '-f', 'lavfi', '-i', 'color=c=black:s=320x320',
  43. '-c:v', 'libwebp', '-pix_fmt', 'yuv420p', '-vframes', '1', generated_file,
  44. ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  45. file = test_data_dir + '/foo %d bar/foo_%d.{}'
  46. initial_file = file.format('webp')
  47. os.replace(generated_file, initial_file)
  48. tests = (('webp', 'png'), ('png', 'jpg'))
  49. for inp, out in tests:
  50. out_file = file.format(out)
  51. if os.path.exists(out_file):
  52. os.remove(out_file)
  53. pp.convert_thumbnail(file.format(inp), out)
  54. self.assertTrue(os.path.exists(out_file))
  55. for _, out in tests:
  56. os.remove(file.format(out))
  57. os.remove(initial_file)
  58. class TestExec(unittest.TestCase):
  59. def test_parse_cmd(self):
  60. pp = ExecPP(YoutubeDL(), '')
  61. info = {'filepath': 'file name'}
  62. cmd = 'echo {}'.format(shell_quote(info['filepath']))
  63. self.assertEqual(pp.parse_cmd('echo', info), cmd)
  64. self.assertEqual(pp.parse_cmd('echo {}', info), cmd)
  65. self.assertEqual(pp.parse_cmd('echo %(filepath)q', info), cmd)
  66. class TestModifyChaptersPP(unittest.TestCase):
  67. def setUp(self):
  68. self._pp = ModifyChaptersPP(YoutubeDL())
  69. @staticmethod
  70. def _sponsor_chapter(start, end, cat, remove=False, title=None):
  71. if title is None:
  72. title = SponsorBlockPP.CATEGORIES[cat]
  73. return {
  74. 'start_time': start,
  75. 'end_time': end,
  76. '_categories': [(cat, start, end, title)],
  77. **({'remove': True} if remove else {}),
  78. }
  79. @staticmethod
  80. def _chapter(start, end, title=None, remove=False):
  81. c = {'start_time': start, 'end_time': end}
  82. if title is not None:
  83. c['title'] = title
  84. if remove:
  85. c['remove'] = True
  86. return c
  87. def _chapters(self, ends, titles):
  88. self.assertEqual(len(ends), len(titles))
  89. start = 0
  90. chapters = []
  91. for e, t in zip(ends, titles):
  92. chapters.append(self._chapter(start, e, t))
  93. start = e
  94. return chapters
  95. def _remove_marked_arrange_sponsors_test_impl(
  96. self, chapters, expected_chapters, expected_removed):
  97. actual_chapters, actual_removed = (
  98. self._pp._remove_marked_arrange_sponsors(chapters))
  99. for c in actual_removed:
  100. c.pop('title', None)
  101. c.pop('_categories', None)
  102. actual_chapters = [{
  103. 'start_time': c['start_time'],
  104. 'end_time': c['end_time'],
  105. 'title': c['title'],
  106. } for c in actual_chapters]
  107. self.assertSequenceEqual(expected_chapters, actual_chapters)
  108. self.assertSequenceEqual(expected_removed, actual_removed)
  109. def test_remove_marked_arrange_sponsors_CanGetThroughUnaltered(self):
  110. chapters = self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4'])
  111. self._remove_marked_arrange_sponsors_test_impl(chapters, chapters, [])
  112. def test_remove_marked_arrange_sponsors_ChapterWithSponsors(self):
  113. chapters = [
  114. *self._chapters([70], ['c']),
  115. self._sponsor_chapter(10, 20, 'sponsor'),
  116. self._sponsor_chapter(30, 40, 'preview'),
  117. self._sponsor_chapter(50, 60, 'filler')]
  118. expected = self._chapters(
  119. [10, 20, 30, 40, 50, 60, 70],
  120. ['c', '[SponsorBlock]: Sponsor', 'c', '[SponsorBlock]: Preview/Recap',
  121. 'c', '[SponsorBlock]: Filler Tangent', 'c'])
  122. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  123. def test_remove_marked_arrange_sponsors_SponsorBlockChapters(self):
  124. chapters = [
  125. *self._chapters([70], ['c']),
  126. self._sponsor_chapter(10, 20, 'chapter', title='sb c1'),
  127. self._sponsor_chapter(15, 16, 'chapter', title='sb c2'),
  128. self._sponsor_chapter(30, 40, 'preview'),
  129. self._sponsor_chapter(50, 60, 'filler')]
  130. expected = self._chapters(
  131. [10, 15, 16, 20, 30, 40, 50, 60, 70],
  132. ['c', '[SponsorBlock]: sb c1', '[SponsorBlock]: sb c1, sb c2', '[SponsorBlock]: sb c1',
  133. 'c', '[SponsorBlock]: Preview/Recap',
  134. 'c', '[SponsorBlock]: Filler Tangent', 'c'])
  135. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  136. def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self):
  137. chapters = [
  138. *self._chapters([120], ['c']),
  139. self._sponsor_chapter(10, 45, 'sponsor'),
  140. self._sponsor_chapter(20, 40, 'selfpromo'),
  141. self._sponsor_chapter(50, 70, 'sponsor'),
  142. self._sponsor_chapter(60, 85, 'selfpromo'),
  143. self._sponsor_chapter(90, 120, 'selfpromo'),
  144. self._sponsor_chapter(100, 110, 'sponsor')]
  145. expected = self._chapters(
  146. [10, 20, 40, 45, 50, 60, 70, 85, 90, 100, 110, 120],
  147. ['c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Sponsor, Unpaid/Self Promotion',
  148. '[SponsorBlock]: Sponsor',
  149. 'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Sponsor, Unpaid/Self Promotion',
  150. '[SponsorBlock]: Unpaid/Self Promotion',
  151. 'c', '[SponsorBlock]: Unpaid/Self Promotion', '[SponsorBlock]: Unpaid/Self Promotion, Sponsor',
  152. '[SponsorBlock]: Unpaid/Self Promotion'])
  153. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  154. def test_remove_marked_arrange_sponsors_ChapterWithCuts(self):
  155. cuts = [self._chapter(10, 20, remove=True),
  156. self._sponsor_chapter(30, 40, 'sponsor', remove=True),
  157. self._chapter(50, 60, remove=True)]
  158. chapters = self._chapters([70], ['c']) + cuts
  159. self._remove_marked_arrange_sponsors_test_impl(
  160. chapters, self._chapters([40], ['c']), cuts)
  161. def test_remove_marked_arrange_sponsors_ChapterWithSponsorsAndCuts(self):
  162. chapters = [
  163. *self._chapters([70], ['c']),
  164. self._sponsor_chapter(10, 20, 'sponsor'),
  165. self._sponsor_chapter(30, 40, 'selfpromo', remove=True),
  166. self._sponsor_chapter(50, 60, 'interaction')]
  167. expected = self._chapters([10, 20, 40, 50, 60],
  168. ['c', '[SponsorBlock]: Sponsor', 'c',
  169. '[SponsorBlock]: Interaction Reminder', 'c'])
  170. self._remove_marked_arrange_sponsors_test_impl(
  171. chapters, expected, [self._chapter(30, 40, remove=True)])
  172. def test_remove_marked_arrange_sponsors_ChapterWithSponsorCutInTheMiddle(self):
  173. cuts = [self._sponsor_chapter(20, 30, 'selfpromo', remove=True),
  174. self._chapter(40, 50, remove=True)]
  175. chapters = [
  176. *self._chapters([70], ['c']),
  177. self._sponsor_chapter(10, 60, 'sponsor'),
  178. *cuts]
  179. expected = self._chapters(
  180. [10, 40, 50], ['c', '[SponsorBlock]: Sponsor', 'c'])
  181. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  182. def test_remove_marked_arrange_sponsors_ChapterWithCutHidingSponsor(self):
  183. cuts = [self._sponsor_chapter(20, 50, 'selfpromo', remove=True)]
  184. chapters = [
  185. *self._chapters([60], ['c']),
  186. self._sponsor_chapter(10, 20, 'intro'),
  187. self._sponsor_chapter(30, 40, 'sponsor'),
  188. self._sponsor_chapter(50, 60, 'outro'),
  189. *cuts]
  190. expected = self._chapters(
  191. [10, 20, 30], ['c', '[SponsorBlock]: Intermission/Intro Animation', '[SponsorBlock]: Endcards/Credits'])
  192. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  193. def test_remove_marked_arrange_sponsors_ChapterWithAdjacentSponsors(self):
  194. chapters = [
  195. *self._chapters([70], ['c']),
  196. self._sponsor_chapter(10, 20, 'sponsor'),
  197. self._sponsor_chapter(20, 30, 'selfpromo'),
  198. self._sponsor_chapter(30, 40, 'interaction')]
  199. expected = self._chapters(
  200. [10, 20, 30, 40, 70],
  201. ['c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion',
  202. '[SponsorBlock]: Interaction Reminder', 'c'])
  203. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  204. def test_remove_marked_arrange_sponsors_ChapterWithAdjacentCuts(self):
  205. chapters = [
  206. *self._chapters([70], ['c']),
  207. self._sponsor_chapter(10, 20, 'sponsor'),
  208. self._sponsor_chapter(20, 30, 'interaction', remove=True),
  209. self._chapter(30, 40, remove=True),
  210. self._sponsor_chapter(40, 50, 'selfpromo', remove=True),
  211. self._sponsor_chapter(50, 60, 'interaction')]
  212. expected = self._chapters([10, 20, 30, 40],
  213. ['c', '[SponsorBlock]: Sponsor',
  214. '[SponsorBlock]: Interaction Reminder', 'c'])
  215. self._remove_marked_arrange_sponsors_test_impl(
  216. chapters, expected, [self._chapter(20, 50, remove=True)])
  217. def test_remove_marked_arrange_sponsors_ChapterWithOverlappingSponsors(self):
  218. chapters = [
  219. *self._chapters([70], ['c']),
  220. self._sponsor_chapter(10, 30, 'sponsor'),
  221. self._sponsor_chapter(20, 50, 'selfpromo'),
  222. self._sponsor_chapter(40, 60, 'interaction')]
  223. expected = self._chapters(
  224. [10, 20, 30, 40, 50, 60, 70],
  225. ['c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Sponsor, Unpaid/Self Promotion',
  226. '[SponsorBlock]: Unpaid/Self Promotion', '[SponsorBlock]: Unpaid/Self Promotion, Interaction Reminder',
  227. '[SponsorBlock]: Interaction Reminder', 'c'])
  228. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  229. def test_remove_marked_arrange_sponsors_ChapterWithOverlappingCuts(self):
  230. chapters = [
  231. *self._chapters([70], ['c']),
  232. self._sponsor_chapter(10, 30, 'sponsor', remove=True),
  233. self._sponsor_chapter(20, 50, 'selfpromo', remove=True),
  234. self._sponsor_chapter(40, 60, 'interaction', remove=True)]
  235. self._remove_marked_arrange_sponsors_test_impl(
  236. chapters, self._chapters([20], ['c']), [self._chapter(10, 60, remove=True)])
  237. def test_remove_marked_arrange_sponsors_ChapterWithRunsOfOverlappingSponsors(self):
  238. chapters = [
  239. *self._chapters([170], ['c']),
  240. self._sponsor_chapter(0, 30, 'intro'),
  241. self._sponsor_chapter(20, 50, 'sponsor'),
  242. self._sponsor_chapter(40, 60, 'selfpromo'),
  243. self._sponsor_chapter(70, 90, 'sponsor'),
  244. self._sponsor_chapter(80, 100, 'sponsor'),
  245. self._sponsor_chapter(90, 110, 'sponsor'),
  246. self._sponsor_chapter(120, 140, 'selfpromo'),
  247. self._sponsor_chapter(130, 160, 'interaction'),
  248. self._sponsor_chapter(150, 170, 'outro')]
  249. expected = self._chapters(
  250. [20, 30, 40, 50, 60, 70, 110, 120, 130, 140, 150, 160, 170],
  251. ['[SponsorBlock]: Intermission/Intro Animation', '[SponsorBlock]: Intermission/Intro Animation, Sponsor', '[SponsorBlock]: Sponsor',
  252. '[SponsorBlock]: Sponsor, Unpaid/Self Promotion', '[SponsorBlock]: Unpaid/Self Promotion', 'c',
  253. '[SponsorBlock]: Sponsor', 'c', '[SponsorBlock]: Unpaid/Self Promotion',
  254. '[SponsorBlock]: Unpaid/Self Promotion, Interaction Reminder',
  255. '[SponsorBlock]: Interaction Reminder',
  256. '[SponsorBlock]: Interaction Reminder, Endcards/Credits', '[SponsorBlock]: Endcards/Credits'])
  257. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  258. def test_remove_marked_arrange_sponsors_ChapterWithRunsOfOverlappingCuts(self):
  259. chapters = [
  260. *self._chapters([170], ['c']),
  261. self._chapter(0, 30, remove=True),
  262. self._sponsor_chapter(20, 50, 'sponsor', remove=True),
  263. self._chapter(40, 60, remove=True),
  264. self._sponsor_chapter(70, 90, 'sponsor', remove=True),
  265. self._chapter(80, 100, remove=True),
  266. self._chapter(90, 110, remove=True),
  267. self._sponsor_chapter(120, 140, 'sponsor', remove=True),
  268. self._sponsor_chapter(130, 160, 'selfpromo', remove=True),
  269. self._chapter(150, 170, remove=True)]
  270. expected_cuts = [self._chapter(0, 60, remove=True),
  271. self._chapter(70, 110, remove=True),
  272. self._chapter(120, 170, remove=True)]
  273. self._remove_marked_arrange_sponsors_test_impl(
  274. chapters, self._chapters([20], ['c']), expected_cuts)
  275. def test_remove_marked_arrange_sponsors_OverlappingSponsorsDifferentTitlesAfterCut(self):
  276. chapters = [
  277. *self._chapters([60], ['c']),
  278. self._sponsor_chapter(10, 60, 'sponsor'),
  279. self._sponsor_chapter(10, 40, 'intro'),
  280. self._sponsor_chapter(30, 50, 'interaction'),
  281. self._sponsor_chapter(30, 50, 'selfpromo', remove=True),
  282. self._sponsor_chapter(40, 50, 'interaction'),
  283. self._sponsor_chapter(50, 60, 'outro')]
  284. expected = self._chapters(
  285. [10, 30, 40], ['c', '[SponsorBlock]: Sponsor, Intermission/Intro Animation', '[SponsorBlock]: Sponsor, Endcards/Credits'])
  286. self._remove_marked_arrange_sponsors_test_impl(
  287. chapters, expected, [self._chapter(30, 50, remove=True)])
  288. def test_remove_marked_arrange_sponsors_SponsorsNoLongerOverlapAfterCut(self):
  289. chapters = [
  290. *self._chapters([70], ['c']),
  291. self._sponsor_chapter(10, 30, 'sponsor'),
  292. self._sponsor_chapter(20, 50, 'interaction'),
  293. self._sponsor_chapter(30, 50, 'selfpromo', remove=True),
  294. self._sponsor_chapter(40, 60, 'sponsor'),
  295. self._sponsor_chapter(50, 60, 'interaction')]
  296. expected = self._chapters(
  297. [10, 20, 40, 50], ['c', '[SponsorBlock]: Sponsor',
  298. '[SponsorBlock]: Sponsor, Interaction Reminder', 'c'])
  299. self._remove_marked_arrange_sponsors_test_impl(
  300. chapters, expected, [self._chapter(30, 50, remove=True)])
  301. def test_remove_marked_arrange_sponsors_SponsorsStillOverlapAfterCut(self):
  302. chapters = [
  303. *self._chapters([70], ['c']),
  304. self._sponsor_chapter(10, 60, 'sponsor'),
  305. self._sponsor_chapter(20, 60, 'interaction'),
  306. self._sponsor_chapter(30, 50, 'selfpromo', remove=True)]
  307. expected = self._chapters(
  308. [10, 20, 40, 50], ['c', '[SponsorBlock]: Sponsor',
  309. '[SponsorBlock]: Sponsor, Interaction Reminder', 'c'])
  310. self._remove_marked_arrange_sponsors_test_impl(
  311. chapters, expected, [self._chapter(30, 50, remove=True)])
  312. def test_remove_marked_arrange_sponsors_ChapterWithRunsOfOverlappingSponsorsAndCuts(self):
  313. chapters = [
  314. *self._chapters([200], ['c']),
  315. self._sponsor_chapter(10, 40, 'sponsor'),
  316. self._sponsor_chapter(10, 30, 'intro'),
  317. self._chapter(20, 30, remove=True),
  318. self._sponsor_chapter(30, 40, 'selfpromo'),
  319. self._sponsor_chapter(50, 70, 'sponsor'),
  320. self._sponsor_chapter(60, 80, 'interaction'),
  321. self._chapter(70, 80, remove=True),
  322. self._sponsor_chapter(70, 90, 'sponsor'),
  323. self._sponsor_chapter(80, 100, 'interaction'),
  324. self._sponsor_chapter(120, 170, 'selfpromo'),
  325. self._sponsor_chapter(130, 180, 'outro'),
  326. self._chapter(140, 150, remove=True),
  327. self._chapter(150, 160, remove=True)]
  328. expected = self._chapters(
  329. [10, 20, 30, 40, 50, 70, 80, 100, 110, 130, 140, 160],
  330. ['c', '[SponsorBlock]: Sponsor, Intermission/Intro Animation', '[SponsorBlock]: Sponsor, Unpaid/Self Promotion',
  331. 'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Sponsor, Interaction Reminder',
  332. '[SponsorBlock]: Interaction Reminder', 'c', '[SponsorBlock]: Unpaid/Self Promotion',
  333. '[SponsorBlock]: Unpaid/Self Promotion, Endcards/Credits', '[SponsorBlock]: Endcards/Credits', 'c'])
  334. expected_cuts = [self._chapter(20, 30, remove=True),
  335. self._chapter(70, 80, remove=True),
  336. self._chapter(140, 160, remove=True)]
  337. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, expected_cuts)
  338. def test_remove_marked_arrange_sponsors_SponsorOverlapsMultipleChapters(self):
  339. chapters = [
  340. *self._chapters([20, 40, 60, 80, 100], ['c1', 'c2', 'c3', 'c4', 'c5']),
  341. self._sponsor_chapter(10, 90, 'sponsor')]
  342. expected = self._chapters([10, 90, 100], ['c1', '[SponsorBlock]: Sponsor', 'c5'])
  343. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  344. def test_remove_marked_arrange_sponsors_CutOverlapsMultipleChapters(self):
  345. cuts = [self._chapter(10, 90, remove=True)]
  346. chapters = self._chapters([20, 40, 60, 80, 100], ['c1', 'c2', 'c3', 'c4', 'c5']) + cuts
  347. expected = self._chapters([10, 20], ['c1', 'c5'])
  348. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  349. def test_remove_marked_arrange_sponsors_SponsorsWithinSomeChaptersAndOverlappingOthers(self):
  350. chapters = [
  351. *self._chapters([10, 40, 60, 80], ['c1', 'c2', 'c3', 'c4']),
  352. self._sponsor_chapter(20, 30, 'sponsor'),
  353. self._sponsor_chapter(50, 70, 'selfpromo')]
  354. expected = self._chapters([10, 20, 30, 40, 50, 70, 80],
  355. ['c1', 'c2', '[SponsorBlock]: Sponsor', 'c2', 'c3',
  356. '[SponsorBlock]: Unpaid/Self Promotion', 'c4'])
  357. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  358. def test_remove_marked_arrange_sponsors_CutsWithinSomeChaptersAndOverlappingOthers(self):
  359. cuts = [self._chapter(20, 30, remove=True), self._chapter(50, 70, remove=True)]
  360. chapters = self._chapters([10, 40, 60, 80], ['c1', 'c2', 'c3', 'c4']) + cuts
  361. expected = self._chapters([10, 30, 40, 50], ['c1', 'c2', 'c3', 'c4'])
  362. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  363. def test_remove_marked_arrange_sponsors_ChaptersAfterLastSponsor(self):
  364. chapters = [
  365. *self._chapters([20, 40, 50, 60], ['c1', 'c2', 'c3', 'c4']),
  366. self._sponsor_chapter(10, 30, 'music_offtopic')]
  367. expected = self._chapters(
  368. [10, 30, 40, 50, 60],
  369. ['c1', '[SponsorBlock]: Non-Music Section', 'c2', 'c3', 'c4'])
  370. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  371. def test_remove_marked_arrange_sponsors_ChaptersAfterLastCut(self):
  372. cuts = [self._chapter(10, 30, remove=True)]
  373. chapters = self._chapters([20, 40, 50, 60], ['c1', 'c2', 'c3', 'c4']) + cuts
  374. expected = self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4'])
  375. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  376. def test_remove_marked_arrange_sponsors_SponsorStartsAtChapterStart(self):
  377. chapters = [
  378. *self._chapters([10, 20, 40], ['c1', 'c2', 'c3']),
  379. self._sponsor_chapter(20, 30, 'sponsor')]
  380. expected = self._chapters([10, 20, 30, 40], ['c1', 'c2', '[SponsorBlock]: Sponsor', 'c3'])
  381. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  382. def test_remove_marked_arrange_sponsors_CutStartsAtChapterStart(self):
  383. cuts = [self._chapter(20, 30, remove=True)]
  384. chapters = self._chapters([10, 20, 40], ['c1', 'c2', 'c3']) + cuts
  385. expected = self._chapters([10, 20, 30], ['c1', 'c2', 'c3'])
  386. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  387. def test_remove_marked_arrange_sponsors_SponsorEndsAtChapterEnd(self):
  388. chapters = [
  389. *self._chapters([10, 30, 40], ['c1', 'c2', 'c3']),
  390. self._sponsor_chapter(20, 30, 'sponsor')]
  391. expected = self._chapters([10, 20, 30, 40], ['c1', 'c2', '[SponsorBlock]: Sponsor', 'c3'])
  392. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  393. def test_remove_marked_arrange_sponsors_CutEndsAtChapterEnd(self):
  394. cuts = [self._chapter(20, 30, remove=True)]
  395. chapters = self._chapters([10, 30, 40], ['c1', 'c2', 'c3']) + cuts
  396. expected = self._chapters([10, 20, 30], ['c1', 'c2', 'c3'])
  397. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  398. def test_remove_marked_arrange_sponsors_SponsorCoincidesWithChapters(self):
  399. chapters = [
  400. *self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4']),
  401. self._sponsor_chapter(10, 30, 'sponsor')]
  402. expected = self._chapters([10, 30, 40], ['c1', '[SponsorBlock]: Sponsor', 'c4'])
  403. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  404. def test_remove_marked_arrange_sponsors_CutCoincidesWithChapters(self):
  405. cuts = [self._chapter(10, 30, remove=True)]
  406. chapters = self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4']) + cuts
  407. expected = self._chapters([10, 20], ['c1', 'c4'])
  408. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  409. def test_remove_marked_arrange_sponsors_SponsorsAtVideoBoundaries(self):
  410. chapters = [
  411. *self._chapters([20, 40, 60], ['c1', 'c2', 'c3']),
  412. self._sponsor_chapter(0, 10, 'intro'), self._sponsor_chapter(50, 60, 'outro')]
  413. expected = self._chapters(
  414. [10, 20, 40, 50, 60], ['[SponsorBlock]: Intermission/Intro Animation', 'c1', 'c2', 'c3', '[SponsorBlock]: Endcards/Credits'])
  415. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  416. def test_remove_marked_arrange_sponsors_CutsAtVideoBoundaries(self):
  417. cuts = [self._chapter(0, 10, remove=True), self._chapter(50, 60, remove=True)]
  418. chapters = self._chapters([20, 40, 60], ['c1', 'c2', 'c3']) + cuts
  419. expected = self._chapters([10, 30, 40], ['c1', 'c2', 'c3'])
  420. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  421. def test_remove_marked_arrange_sponsors_SponsorsOverlapChaptersAtVideoBoundaries(self):
  422. chapters = [
  423. *self._chapters([10, 40, 50], ['c1', 'c2', 'c3']),
  424. self._sponsor_chapter(0, 20, 'intro'),
  425. self._sponsor_chapter(30, 50, 'outro')]
  426. expected = self._chapters(
  427. [20, 30, 50], ['[SponsorBlock]: Intermission/Intro Animation', 'c2', '[SponsorBlock]: Endcards/Credits'])
  428. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  429. def test_remove_marked_arrange_sponsors_CutsOverlapChaptersAtVideoBoundaries(self):
  430. cuts = [self._chapter(0, 20, remove=True), self._chapter(30, 50, remove=True)]
  431. chapters = self._chapters([10, 40, 50], ['c1', 'c2', 'c3']) + cuts
  432. expected = self._chapters([10], ['c2'])
  433. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, cuts)
  434. def test_remove_marked_arrange_sponsors_EverythingSponsored(self):
  435. chapters = [
  436. *self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4']),
  437. self._sponsor_chapter(0, 20, 'intro'),
  438. self._sponsor_chapter(20, 40, 'outro')]
  439. expected = self._chapters([20, 40], ['[SponsorBlock]: Intermission/Intro Animation', '[SponsorBlock]: Endcards/Credits'])
  440. self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
  441. def test_remove_marked_arrange_sponsors_EverythingCut(self):
  442. cuts = [self._chapter(0, 20, remove=True), self._chapter(20, 40, remove=True)]
  443. chapters = self._chapters([10, 20, 30, 40], ['c1', 'c2', 'c3', 'c4']) + cuts
  444. self._remove_marked_arrange_sponsors_test_impl(
  445. chapters, [], [self._chapter(0, 40, remove=True)])
  446. def test_remove_marked_arrange_sponsors_TinyChaptersInTheOriginalArePreserved(self):
  447. chapters = self._chapters([0.1, 0.2, 0.3, 0.4], ['c1', 'c2', 'c3', 'c4'])
  448. self._remove_marked_arrange_sponsors_test_impl(chapters, chapters, [])
  449. def test_remove_marked_arrange_sponsors_TinySponsorsAreIgnored(self):
  450. chapters = [self._sponsor_chapter(0, 0.1, 'intro'), self._chapter(0.1, 0.2, 'c1'),
  451. self._sponsor_chapter(0.2, 0.3, 'sponsor'), self._chapter(0.3, 0.4, 'c2'),
  452. self._sponsor_chapter(0.4, 0.5, 'outro')]
  453. self._remove_marked_arrange_sponsors_test_impl(
  454. chapters, self._chapters([0.3, 0.5], ['c1', 'c2']), [])
  455. def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromCutsAreIgnored(self):
  456. cuts = [self._chapter(1.5, 2.5, remove=True)]
  457. chapters = self._chapters([2, 3, 3.5], ['c1', 'c2', 'c3']) + cuts
  458. self._remove_marked_arrange_sponsors_test_impl(
  459. chapters, self._chapters([2, 2.5], ['c1', 'c3']), cuts)
  460. def test_remove_marked_arrange_sponsors_SingleTinyChapterIsPreserved(self):
  461. cuts = [self._chapter(0.5, 2, remove=True)]
  462. chapters = self._chapters([2], ['c']) + cuts
  463. self._remove_marked_arrange_sponsors_test_impl(
  464. chapters, self._chapters([0.5], ['c']), cuts)
  465. def test_remove_marked_arrange_sponsors_TinyChapterAtTheStartPrependedToTheNext(self):
  466. cuts = [self._chapter(0.5, 2, remove=True)]
  467. chapters = self._chapters([2, 4], ['c1', 'c2']) + cuts
  468. self._remove_marked_arrange_sponsors_test_impl(
  469. chapters, self._chapters([2.5], ['c2']), cuts)
  470. def test_remove_marked_arrange_sponsors_TinyChaptersResultingFromSponsorOverlapAreIgnored(self):
  471. chapters = [
  472. *self._chapters([1, 3, 4], ['c1', 'c2', 'c3']),
  473. self._sponsor_chapter(1.5, 2.5, 'sponsor')]
  474. self._remove_marked_arrange_sponsors_test_impl(
  475. chapters, self._chapters([1.5, 2.5, 4], ['c1', '[SponsorBlock]: Sponsor', 'c3']), [])
  476. def test_remove_marked_arrange_sponsors_TinySponsorsOverlapsAreIgnored(self):
  477. chapters = [
  478. *self._chapters([2, 3, 5], ['c1', 'c2', 'c3']),
  479. self._sponsor_chapter(1, 3, 'sponsor'),
  480. self._sponsor_chapter(2.5, 4, 'selfpromo')]
  481. self._remove_marked_arrange_sponsors_test_impl(
  482. chapters, self._chapters([1, 3, 4, 5], [
  483. 'c1', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion', 'c3']), [])
  484. def test_remove_marked_arrange_sponsors_TinySponsorsPrependedToTheNextSponsor(self):
  485. chapters = [
  486. *self._chapters([4], ['c']),
  487. self._sponsor_chapter(1.5, 2, 'sponsor'),
  488. self._sponsor_chapter(2, 4, 'selfpromo')]
  489. self._remove_marked_arrange_sponsors_test_impl(
  490. chapters, self._chapters([1.5, 4], ['c', '[SponsorBlock]: Unpaid/Self Promotion']), [])
  491. def test_remove_marked_arrange_sponsors_SmallestSponsorInTheOverlapGetsNamed(self):
  492. self._pp._sponsorblock_chapter_title = '[SponsorBlock]: %(name)s'
  493. chapters = [
  494. *self._chapters([10], ['c']),
  495. self._sponsor_chapter(2, 8, 'sponsor'),
  496. self._sponsor_chapter(4, 6, 'selfpromo')]
  497. self._remove_marked_arrange_sponsors_test_impl(
  498. chapters, self._chapters([2, 4, 6, 8, 10], [
  499. 'c', '[SponsorBlock]: Sponsor', '[SponsorBlock]: Unpaid/Self Promotion',
  500. '[SponsorBlock]: Sponsor', 'c',
  501. ]), [])
  502. def test_make_concat_opts_CommonCase(self):
  503. sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')]
  504. expected = '''ffconcat version 1.0
  505. file 'file:test'
  506. outpoint 1.000000
  507. file 'file:test'
  508. inpoint 2.000000
  509. outpoint 10.000000
  510. file 'file:test'
  511. inpoint 20.000000
  512. '''
  513. opts = self._pp._make_concat_opts(sponsor_chapters, 30)
  514. self.assertEqual(expected, ''.join(self._pp._concat_spec(['test'] * len(opts), opts)))
  515. def test_make_concat_opts_NoZeroDurationChunkAtVideoStart(self):
  516. sponsor_chapters = [self._chapter(0, 1, 's1'), self._chapter(10, 20, 's2')]
  517. expected = '''ffconcat version 1.0
  518. file 'file:test'
  519. inpoint 1.000000
  520. outpoint 10.000000
  521. file 'file:test'
  522. inpoint 20.000000
  523. '''
  524. opts = self._pp._make_concat_opts(sponsor_chapters, 30)
  525. self.assertEqual(expected, ''.join(self._pp._concat_spec(['test'] * len(opts), opts)))
  526. def test_make_concat_opts_NoZeroDurationChunkAtVideoEnd(self):
  527. sponsor_chapters = [self._chapter(1, 2, 's1'), self._chapter(10, 20, 's2')]
  528. expected = '''ffconcat version 1.0
  529. file 'file:test'
  530. outpoint 1.000000
  531. file 'file:test'
  532. inpoint 2.000000
  533. outpoint 10.000000
  534. '''
  535. opts = self._pp._make_concat_opts(sponsor_chapters, 20)
  536. self.assertEqual(expected, ''.join(self._pp._concat_spec(['test'] * len(opts), opts)))
  537. def test_quote_for_concat_RunsOfQuotes(self):
  538. self.assertEqual(
  539. r"'special '\'' '\'\''characters'\'\'\''galore'",
  540. self._pp._quote_for_ffmpeg("special ' ''characters'''galore"))
  541. def test_quote_for_concat_QuotesAtStart(self):
  542. self.assertEqual(
  543. r"\'\'\''special '\'' characters '\'' galore'",
  544. self._pp._quote_for_ffmpeg("'''special ' characters ' galore"))
  545. def test_quote_for_concat_QuotesAtEnd(self):
  546. self.assertEqual(
  547. r"'special '\'' characters '\'' galore'\'\'\'",
  548. self._pp._quote_for_ffmpeg("special ' characters ' galore'''"))
  549. if __name__ == '__main__':
  550. unittest.main()