test_postprocessors.py 30 KB

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