1415 Commits

Author SHA1 Message Date
Ashley
e903cb5bfc bump lts version :3 2023-04-02 16:48:19 +00:00
Ashley
def9958066 fix channels in lts version 2023-04-02 16:47:02 +00:00
Ashley
0adefa5d64 remove fonts.poketube.fun :3 2023-04-02 16:44:30 +00:00
Ashley
4339653efb add lts branch 2023-03-13 13:52:06 +00:00
Ashley
3cc7a3696e relase non lts version 2023-03-12 20:38:10 +00:00
Ashley
e3808d9794 bump ver 2023-03-12 20:10:50 +00:00
Ashley
0332f23c83 bump version :3 2023-03-12 20:10:13 +00:00
Ashley
4c4a127be3 fix channels!!!!!! 2023-03-12 20:05:28 +00:00
Ashley
fb0cc80a0f fix mobile stuff owo 2023-03-12 18:01:11 +00:00
Ashley
1e9016d4bc whoa cool stuff 2023-03-12 18:00:14 +00:00
Ashley
5c3adff6fe fix continuations owo 2023-03-12 17:58:19 +00:00
Ashley
afe1fe664a fix a goofy ahh typo 2023-03-12 17:57:05 +00:00
Ashley
b199a44fe3 add ambient music to main page :3 2023-03-12 13:06:29 +00:00
Ashley
ecfc637511 add ambient mode 3.0 to universe 2 2023-03-12 13:05:08 +00:00
Ashley
99791515ab add ambient music !! 2023-03-12 07:29:19 +00:00
Ashley
51847e7e51 use libcurl owowowowowowow 2023-03-12 06:41:06 +00:00
Ashley
b9ae02d4eb remove ip api 2023-03-12 06:39:49 +00:00
Ashley
1535dd48ee ad libcurl owowowowow :3 2023-03-12 06:39:04 +00:00
Ashley
9e2cb94e92 use libcurl instead 2023-03-12 06:38:21 +00:00
Ashley
ae0336ad05 add try catch :p 2023-03-11 16:03:14 +00:00
Ashley
7057c6628a whoa cool stuff 2023-03-11 15:59:48 +00:00
Ashley
d79322b53f add -or-later 2023-03-11 15:41:43 +00:00
Ashley
b24c29510a add minifiycss 2023-03-11 14:34:31 +00:00
Ashley
0359c6027f add clean css ıowowowow 2023-03-11 14:32:12 +00:00
Ashley
b94471343e this is definitely one of the commits ever 2023-03-11 13:41:12 +00:00
Ashley
0cfa1e9780 fix some issues :3 2023-03-11 13:39:44 +00:00
Ashley
7026d81c84 remove other urls 2023-03-11 13:33:19 +00:00
Ashley
d10496df39 cool animation stuff 2023-03-10 20:58:17 +00:00
Ashley
52aae41ae7 re-add support for opera FOR NOW 2023-03-10 17:34:11 +00:00
Ashley
b7d9559b1e remove border radius when da video is full screen owowowowowowowowo 2023-03-10 15:37:14 +00:00
Ashley
290ed76a02 remove support for opera and opera based browsers 2023-03-10 15:35:45 +00:00
Ashley
c25e73b8a0 whoa cool stuff and good stuff 2023-03-10 13:45:36 +00:00
Ashley
a7541f75f1 fix stuff 2023-03-09 20:32:28 +00:00
Ashley
d3206151e3 this is definitely one of the commits ever 2023-03-09 19:42:40 +00:00
Ashley
e613d8a742 add cache to returnyoutubedislike owo 2023-03-09 19:41:01 +00:00
Ashley
bfafd7a01e use local json instead because api.inv.io slooooooooooooooow 2023-03-09 18:50:38 +00:00
Ashley
6cb3ad5fb1 change returnytdislike api owo 2023-03-09 18:05:44 +00:00
Ashley
38d0795cda add invapi.json lol 2023-03-09 18:04:11 +00:00
Ashley
d2df0988c0 change api owo 2023-03-09 15:53:14 +00:00
Ashley
49fde4bc1c change api owo 2023-03-09 15:52:30 +00:00
Ashley
cf0301c902 change api url :3 2023-03-09 15:51:54 +00:00
Ashley
eedbfa5e6d cache moment 2023-03-09 15:50:19 +00:00
Ashley
1aa70f01bb change api url :3 2023-03-09 15:49:37 +00:00
Ashley
f842540864 change api url :3 2023-03-09 15:48:55 +00:00
Ashley
067f318958 Add codec warnin 2023-03-07 19:57:11 +00:00
Ashley
a42a0463d8 fix stuff whoa cool yeah yeah 2023-03-07 18:08:15 +00:00
Ashley
c1f814147b fix stuff hehehe 2023-03-07 18:05:21 +00:00
Ashley
6d111fcfa9 ah yes the 2023-03-07 17:22:18 +00:00
Ashley
f118dbdbef im out of commit messages pwese help me aaaaaaa 2023-03-07 17:21:34 +00:00
Ashley
2584bacb27 yes yes new api yes yes 2023-03-07 17:20:08 +00:00
Ashley
77bc7470aa add new api stuff :3 2023-03-07 16:13:27 +00:00
Ashley
093f35cdf2 add loading stuff :3 2023-03-06 19:58:37 +00:00
Ashley
372e99d6c3 change timeout owo 2023-03-06 19:35:40 +00:00
Ashley
d94ecf6136 change the svg hehe 2023-03-06 16:32:27 +00:00
Ashley
b27b53c264 yea qwq 2023-03-05 20:37:43 +00:00
Ashley
b78830d5f0 add loading stuff :3 2023-03-05 20:36:20 +00:00
Ashley
47bfde44ec fix dis lel 2023-03-05 18:04:50 +00:00
Ashley
80abf5b7aa fix stu 2023-03-05 13:28:34 +00:00
Ashley
cc550aed34 hehe :3 2023-03-05 12:22:21 +00:00
Ashley
5cdb78eca3 fix stuff hehehe 2023-03-05 12:20:41 +00:00
Ashley
d2c1092eae change api owo 2023-03-05 12:19:14 +00:00
Ashley
dce03da3d4 add href hehe 2023-03-05 07:11:26 +00:00
Ashley
20fd255c9f remove br hehe 2023-03-05 07:10:14 +00:00
Ashley
6f5b8e1791 ADD RYF :3 2023-03-05 07:09:02 +00:00
Ashley
396f56272e fix some stuff :3 2023-03-05 05:48:43 +00:00
Ashley
8a2c3e7cef change readme !!!
Signed-off-by: Ashley <iamashley@duck.com>
2023-03-05 05:26:44 +00:00
Ashley
b2d0d59dc1 fix ma skill issue hehe 2023-03-04 20:49:27 +00:00
Ashley
b7f0dc156d add no nonfree codec message :3 2023-03-04 20:48:39 +00:00
Ashley
ccbf7924e1 add atob owo 2023-03-04 16:31:49 +00:00
Ashley
a17df0bafe add referer remover :3 2023-03-04 16:30:57 +00:00
Ashley
2cd214a08f this bug existed and i didint knew it aaaaaaaaaaaaaaaaa 2023-03-04 14:54:21 +00:00
Ashley
241e43ba96 FIX THE DES THINGY HEHEHE 2023-03-04 14:39:43 +00:00
Ashley
b63a595421 change cache control header 2023-03-04 14:26:02 +00:00
Ashley
0f0ffee4d1 change function name uwu 2023-03-04 14:08:54 +00:00
Ashley
b727347446 add universe param :3 2023-03-04 11:07:45 +00:00
Ashley
ee2ab7ea18 add universe :3 2023-03-04 11:01:01 +00:00
Ashley
a437005d9a remove beta labels for autoplay :3 2023-03-03 20:19:23 +00:00
Ashley
9d2efe84a9 add region :3 2023-03-03 18:51:18 +00:00
Ashley
aeb30f63a7 change error codee 2023-03-03 18:49:40 +00:00
Ashley
4dbb7d1852 add page cache owo 2023-03-03 17:50:28 +00:00
Ashley
4840d52749 add cache!! 2023-03-03 17:41:19 +00:00
Ashley
f3cc3dc515 add linkifiy yaayayyaya 2023-03-02 20:42:03 +00:00
Ashley
6842cff39a linkify lmao 2023-03-02 20:40:58 +00:00
Ashley
3888702482 add description check :p 2023-03-02 20:28:18 +00:00
Ashley
d93d23c5a6 Update 'src/libpoketube/libpoketube-core.js' 2023-03-02 20:26:26 +00:00
Ashley
557950ffe3 hehe add erorr 2023-03-02 20:02:50 +00:00
Ashley
a1fdb897c1 reformat file :p 2023-03-02 19:57:20 +00:00
Ashley
fbad53afbd hehe 2023-03-02 19:52:39 +00:00
Ashley
e71c3d676b add embed code hehe 2023-03-02 15:10:23 +00:00
Ashley
88fd5441dd bump version :3 2023-03-02 15:09:11 +00:00
Ashley
3c564806e8 add url owo 2023-03-02 15:07:32 +00:00
Ashley
ce1e7d8473 add try catch :p 2023-03-02 15:05:53 +00:00
Ashley
a0cce7d4a1 add copy code thingy hehe 2023-03-02 15:04:48 +00:00
Ashley
4942d35da3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2023-03-01 18:24:36 +00:00
Ashley
6312a24d66 add le badge 2023-03-01 18:23:39 +00:00
Ashley
ec33351990 add engagement thingy 2023-02-28 14:12:57 +00:00
Ashley
242cb25620 add progressbar owo 2023-02-26 21:02:10 +00:00
Ashley
f0b3592be6 add lazy loading :^ 2023-02-26 20:56:33 +00:00
Ashley
19adaf4797 add progressbar owo 2023-02-26 11:20:43 +00:00
Ashley
c8e3de87fc use getChannelData() instead 2023-02-26 09:34:24 +00:00
Ashley
fa33ae3f5b add more urls :3 2023-02-25 19:39:16 +00:00
Ashley
1879f210b8 bump version :3 2023-02-25 17:50:38 +00:00
Ashley
ce114a9c02 Refactor code :3 2023-02-25 17:48:15 +00:00
Ashley
2c10e36d71 Refactor code :3 2023-02-25 17:47:28 +00:00
Ashley
9d1dab75b0 Refactor code :3 2023-02-25 17:46:42 +00:00
Ashley
cfc4d22f33 fix issues :3 2023-02-25 17:45:15 +00:00
Ashley
96c46abe1f Refactor code :3 2023-02-25 17:20:38 +00:00
Ashley
b149d3e365 Refactor code :3 2023-02-25 17:08:54 +00:00
Ashley
a3d4a07875 Refactor code :3 2023-02-25 17:06:28 +00:00
Ashley
199d509396 add codename :3 2023-02-25 11:42:28 +00:00
Ashley
24217bca4c hehe 2023-02-24 17:37:23 +00:00
Ashley
63f58bc12a hehe 2023-02-24 16:47:08 +00:00
Ashley
a06499ab64 fix some stuff :3 2023-02-24 16:43:03 +00:00
Ashley
c6838b2e86 add proxy :3 2023-02-24 15:55:48 +00:00
Ashley
81fc689f6e add ryd 2023-02-24 15:10:48 +00:00
Ashley
f2f2821122 change api url :3 2023-02-24 14:50:17 +00:00
Ashley
d1a5d00c66 update stuff :p 2023-02-23 18:32:36 +00:00
Ashley
31d90061b4 add redirect lol 2023-02-23 16:04:35 +00:00
Ashley
a348fec5f3 add custom js :3 2023-02-23 16:03:16 +00:00
Ashley
971443c4ba add custom js :3 2023-02-23 16:02:20 +00:00
Ashley
07fe019256 add custom js :3 2023-02-23 16:00:35 +00:00
Ashley
b5f41d61f2 add custom js :3 2023-02-23 15:58:19 +00:00
Ashley
14fbe89618 bump version :3 2023-02-23 15:47:02 +00:00
Ashley
6b23b838c7 hehe try catch 2023-02-22 16:10:47 +00:00
Ashley
dfd2c49845 fix href urls owo 2023-02-22 16:05:10 +00:00
Ashley
0d02f484c4 add try catch :p 2023-02-22 16:03:02 +00:00
Ashley
2b0f354414 add custom themes :3 2023-02-22 16:02:06 +00:00
Ashley
6355800bec add custom css script :3 2023-02-22 16:00:46 +00:00
Ashley
066cc2e2b5 add custom themes :3 2023-02-22 15:59:49 +00:00
Ashley
8be555bd98 add custom css script :3 2023-02-22 15:58:36 +00:00
Ashley
e87e29175f add custom css script :3 2023-02-22 15:58:00 +00:00
Ashley
2d9e112e6e add custom css :3 2023-02-22 15:55:50 +00:00
Ashley
fd2fd9977a add custom css :3 2023-02-22 15:55:08 +00:00
Ashley
48afc4191d add custom css :3 2023-02-22 15:54:13 +00:00
Ashley
d2f5b9f982 add custom css file 2023-02-22 15:53:27 +00:00
Ashley
52203aaf54 Add 'css/custom-css.js' 2023-02-22 15:52:05 +00:00
Ashley
8ea60dff20 bump version :3 2023-02-21 20:20:33 +00:00
Ashley
000d412477 update url :3 2023-02-21 20:19:53 +00:00
Ashley
e5789c4b67 add priv-custom :3 2023-02-20 15:44:30 +00:00
Ashley
e4da73f6eb add priv-custom 2023-02-20 15:42:42 +00:00
Ashley
c99d178f81 support button yaaay :3 2023-02-20 15:31:11 +00:00
Ashley
14d7598abf add support stuff :3 2023-02-20 15:28:52 +00:00
Ashley
dc6a79c67c add apple license 2023-02-17 21:32:37 +00:00
Ashley
813faf0002 add branch lol 2023-02-17 21:27:47 +00:00
Ashley
12bc957d5c bump version :3 2023-02-17 21:23:57 +00:00
Ashley
7758a13e29 add headers :3 2023-02-17 21:22:32 +00:00
Ashley
8bd65125c0 fix stuff :3 2023-02-17 21:19:16 +00:00
Ashley
b740d5869b REFACTOR THE CODEEE BABYYY LESS GOO 2023-02-17 21:17:56 +00:00
Ashley
1fb7f9fa4b center the div :3 2023-02-15 17:30:41 +00:00
Ashley
a393f7e2f5 add poke atmos :3 2023-02-15 17:27:32 +00:00
Ashley
1a7b16ff99 add poke atmos links 2023-02-15 17:26:25 +00:00
Ashley
c4eb353149 poke atmos! 2023-02-15 17:24:15 +00:00
Ashley
c32306a286 update copyright dateu 2023-02-14 15:58:10 +00:00
Ashley
ac6b0e1314 fix a goofy ahh issue 2023-02-14 15:38:36 +00:00
Ashley
1bc29693bb add hover cursor stuff whoa cool 2023-02-14 14:36:39 +00:00
Ashley
f1f0047115 new download :3 2023-02-14 14:30:53 +00:00
Ashley
741456adb8 add hover effects!! 2023-02-13 11:33:23 +00:00
Ashley
797fd4d4ff even more hover effects hell yeah 2023-02-13 11:31:41 +00:00
Ashley
478e668439 add hover efects for thumbnails 2023-02-12 19:06:49 +00:00
Ashley
f87a9691c1 add date params 2023-02-12 19:05:08 +00:00
Ashley
c7099ff62d fix cursor stuff :p 2023-02-12 17:50:45 +00:00
Ashley
ae44de2837 add hover effects!! 2023-02-12 17:48:32 +00:00
Ashley
9900f9d97c channel hover stuff 2023-02-12 17:47:07 +00:00
Ashley
f236ca450d black logo 2023-02-12 17:45:27 +00:00
Ashley
182e274907 add alac licanse 2023-02-12 09:18:31 +00:00
Ashley
8548dc4458 upload alac source code 2023-02-12 09:16:48 +00:00
Ashley
0337286d4b add alac source code - part 4 :3 2023-02-12 09:16:19 +00:00
Ashley
14eaf2c537 add alac source code - part 3 :^ 2023-02-12 09:14:46 +00:00
Ashley
6263204e6d add alac source code - part two :^ 2023-02-12 09:13:40 +00:00
Ashley
9e2213a2af add alac source code - part one 2023-02-12 09:12:54 +00:00
Ashley
30db8164c0 Add 'alac/readme.md' 2023-02-12 09:10:26 +00:00
Ashley
5fe3c9e99f Delete 'CONTRIBUTING.md' 2023-02-12 09:09:27 +00:00
Ashley
fe650421de Delete 'ALACMagicCookieDescription.txt' 2023-02-12 09:09:13 +00:00
Ashley
a600a824b3 Delete 'ReadMe.txt' 2023-02-12 09:09:00 +00:00
Ashley
37017205f0 fix license 2023-02-12 09:08:33 +00:00
Ashley
8b5ba1403e Delete 'PULL_REQUEST_TEMPLATE.md' 2023-02-12 09:07:20 +00:00
Ashley
432363ddef Upload files to '' 2023-02-12 09:06:47 +00:00
Ashley
c36a3017a1 auto play for mobile :3 2023-02-11 13:03:59 +00:00
Ashley
cdd7d3d5f8 add removeparam 2023-02-10 18:28:25 +00:00
Ashley
72603fd12c fix a goofy ahh issue 2023-02-10 18:10:34 +00:00
Ashley
d448d3229d change api url 2023-02-10 17:37:50 +00:00
Ashley
1ea514d5d5 change text lel 2023-02-10 17:12:56 +00:00
Ashley
1e7e4d7bfa fix somethin lel 2023-02-10 16:51:35 +00:00
Ashley
b017d057e4 fix some stuff :3 2023-02-10 16:13:02 +00:00
Ashley
3a9363b460 add auto play :3 2023-02-10 15:14:49 +00:00
Ashley
0b0922ae25 update stuff :p 2023-02-10 13:02:04 +00:00
Ashley
ee5effaa52 MINOR SPELLING MISTAKE 2023-02-10 12:58:31 +00:00
Ashley
da74cc7d3d update stuff :p 2023-02-10 10:34:36 +00:00
Ashley
ef738e07a8 bump version :3 2023-02-09 16:40:16 +00:00
Ashley
ff532a1c9f reformat file :p 2023-02-09 16:36:57 +00:00
Ashley
3f34f718e4 yup, thats the commit lmao 2023-02-09 16:35:22 +00:00
Ashley
d7be518dad relisance file to LGPL 2023-02-09 16:34:45 +00:00
Ashley
3dcfe6715f add robots.txt and prettier site 2023-02-09 16:32:14 +00:00
Ashley
c8364edb60 add robots.txt 2023-02-09 16:30:26 +00:00
Ashley
89c859aedc add earthquake warning 2023-02-09 15:52:07 +00:00
Ashley
c390027e7b add earthquake war 2023-02-09 15:51:27 +00:00
Ashley
4f114c988e add cacher for proxy files 2023-02-06 14:53:42 +00:00
Ashley
2cb38fecbf add cacher for static files 2023-02-06 14:23:19 +00:00
Ashley
436dccd77a whoa cool stuff 2023-02-05 17:58:01 +00:00
Ashley
3fa0e4712f lmao 2023-02-05 15:49:37 +00:00
Ashley
0abca4f7c8 add try catch :p 2023-02-05 12:30:17 +00:00
Ashley
c6dff86894 this is very cool 2023-02-05 11:42:33 +00:00
Ashley
c3b96cf5fe fix some silly issues :3 2023-02-05 08:42:44 +00:00
Ashley
a017c9f478 change api url :3 2023-02-05 08:40:02 +00:00
Ashley
ddf657af91 bump version :3 and add more info 2023-02-04 22:04:44 +00:00
Ashley
fc78d18c9f hehe taggy 2023-02-04 21:59:45 +00:00
Ashley
9d9a3c7d20 hehe tabby 2023-02-04 21:57:13 +00:00
Ashley
3698b2c977 bump version :3 2023-02-04 18:11:08 +00:00
Ashley
b87a0a7df1 chartreuse color :3 2023-02-04 18:09:38 +00:00
Ashley
0bb38c44fe cool stuff 2023-02-04 18:07:57 +00:00
Ashley
cb6904d344 change api url lool 2023-02-04 16:21:20 +00:00
Ashley
bd4534ab0c add live stuff 2023-02-03 14:38:56 +00:00
Ashley
d49d6fab92 fix my goofy ahh issue 2023-02-03 09:26:37 +00:00
Ashley
d12a881dc4 FIX THE FRICKING THE UHHH DID YOU MEAN THINGY IDK ANYMORE PLEASE HELP AAA 2023-02-02 21:14:30 +00:00
Ashley
99b027e85f hehe bai taggy 2023-02-02 19:28:01 +00:00
Ashley
80633ed821 hehe taggy 2023-02-02 19:25:42 +00:00
Ashley
087d1a571d sort by!! 2023-02-02 19:17:50 +00:00
Ashley
43a78e5a92 fix urls :p 2023-02-02 19:16:30 +00:00
Ashley
7fd78e2a15 fixed max widht 2023-02-02 19:15:00 +00:00
Ashley
2ffe1c0e01 tag stuff 2023-02-02 19:12:36 +00:00
Ashley
07d0715ced CONTIUNUANTIONS ARE BACK BABY 2023-02-02 19:11:47 +00:00
Ashley
5abc35ea6c update stuff :p 2023-02-02 17:11:06 +00:00
Ashley
bcf90297ae whoa cool stuff and good stuff 2023-02-02 17:09:42 +00:00
Ashley
c8ac2b0775 AM SORRY 2023-02-02 12:06:11 +00:00
Ashley
a6641805da je 2023-02-02 12:05:37 +00:00
Ashley
e6fe497cf7 hehe ubuntu 2023-02-02 12:03:38 +00:00
Ashley
b4041e0e90 add font-size :3 2023-02-02 10:51:34 +00:00
Ashley
eb1a08db5c new lite stuff :3 2023-02-01 20:19:39 +00:00
Ashley
b9a992a537 yea qwq 2023-02-01 14:01:18 +00:00
Ashley
146194e073 fix a goofy ahh issue 2023-02-01 09:24:51 +00:00
Ashley
7769849334 bump version :3 2023-01-31 20:38:49 +00:00
Ashley
f9e0e59726 add DOCTYPE 2023-01-31 20:38:18 +00:00
Ashley
cb9ee89b1e add verified instance :3 2023-01-31 20:37:42 +00:00
Ashley
2007a53705 add verified instance :3 2023-01-31 20:36:13 +00:00
Ashley
0c52dcf5f9 add ubuntu font :3 2023-01-31 20:31:18 +00:00
Ashley
c21c454a34 new landing :3 2023-01-31 20:28:35 +00:00
Ashley
890e9eac12 add ubuntu font and new borders :3 2023-01-31 20:25:43 +00:00
Ashley
78859e1ae7 add ubuntu font :3 2023-01-31 20:24:56 +00:00
Ashley
fef5938cf6 update urls 2023-01-31 20:21:36 +00:00
Ashley
9dbf1f8b4d bumpy version :3 2023-01-31 13:51:26 +00:00
Ashley
1dd5dc6064 add poketube flex to more stuff :3 2023-01-31 13:50:20 +00:00
Ashley
13539450a4 new iframe :3 2023-01-31 13:48:39 +00:00
Ashley
8e8a9b9af0 Update 'src/libpoketube/init/superinit.js' 2023-01-31 10:57:45 +00:00
Ashley
b2d65bba98 add a check lol 2023-01-31 10:47:34 +00:00
Ashley
fa7a749d76 bump version :3 2023-01-31 10:43:30 +00:00
Ashley
a06b23b61f remove non working ones 2023-01-31 10:05:54 +00:00
Ashley
59770b2c8b Merge pull request 'Add new instance: pt.zzls.xyz' (#26) from Fijxu/poketube:main into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/26
2023-01-31 09:47:05 +00:00
Fijxu
e9531cb330 Add new instance: pt.zzls.xyz 2023-01-30 23:00:53 +00:00
Ashley
48e4905e91 yea qwq 2023-01-30 14:03:19 +00:00
Ashley
95eaae6d48 i like the center lol 2023-01-30 13:48:56 +00:00
Ashley
00389e6660 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2023-01-28 18:27:20 +00:00
Ashley
b9d75c262d whoa cool stuff 2023-01-28 18:26:25 +00:00
Ashley
2860912512 fix my issue ;_; 2023-01-28 14:19:12 +00:00
Ashley
d8c83bde4e add hovered bg lol 2023-01-28 14:12:26 +00:00
Ashley
46b558c6b6 lol 2023-01-28 14:07:40 +00:00
Ashley
8cccb4f1cb yup, thats the commit 2023-01-28 13:42:04 +00:00
Ashley
8bfb81354b bump version :3 2023-01-28 13:37:41 +00:00
Ashley
a28f7cff3a invchannel lol 2023-01-28 13:18:03 +00:00
Ashley
44475d6737 wow cool stuff 2023-01-28 13:04:25 +00:00
Ashley
d1439d7d7d tag stuff 2023-01-28 12:56:16 +00:00
Ashley
32c7d796b9 whoa cool stuff and good stuff
Signed-off-by: Ashley <iamashley@duck.com>
2023-01-26 15:49:31 +00:00
Ashley
c8f7398266 remove hj 2023-01-26 15:06:39 +00:00
Ashley
66b44e14e6 remove watch poketalebotcom 2023-01-26 09:25:57 +00:00
Ashley
73e1b79a5b update stuff :p 2023-01-26 09:24:22 +00:00
Ashley
ca9377a19a add hover cursor stuff whoa cool 2023-01-25 21:09:26 +00:00
Ashley
747b2b2a57 try catch to video as well ig 2023-01-24 09:10:06 +00:00
Ashley
ed7e07d341 add try catch :p 2023-01-24 09:01:44 +00:00
Ashley
5e0e11f057 fix stuff lol 2023-01-23 18:11:17 +00:00
Ashley
0e9bb8e46d center da page owo 2023-01-23 17:20:04 +00:00
Ashley
632cec9be3 add secret-theme :3 2023-01-23 05:31:41 +00:00
Ashley
7b4ae23ddc add secret-theme :3 2023-01-23 05:29:21 +00:00
Ashley
d9deb9b25f add ambient mode to music page!! 2023-01-21 18:35:31 +00:00
Ashley
a6dc06ea2b try artwork :p 2023-01-21 18:34:57 +00:00
Ashley
f3d44e7c6b IM SORRY !BTHGOJHTGKJNYNGNT 2023-01-21 18:06:37 +00:00
Ashley
c52ae0f624 bumpy version :3 2023-01-21 18:04:35 +00:00
Ashley
a5076bd73a whoa cool stuff 2023-01-21 18:03:01 +00:00
Ashley
b54337c25a add music css file!!! 2023-01-21 18:01:54 +00:00
Ashley
8eab3f6840 real :p 2023-01-21 18:00:51 +00:00
Ashley
6a8b6026fa new music url 2023-01-21 17:55:48 +00:00
Ashley
7c13f88baf change version 2023-01-20 20:50:57 +00:00
Ashley
8a8ad1dfb0 my bad qwq 2023-01-20 18:15:30 +00:00
Ashley
0c5ef551fd add secure-poketube-instance 2023-01-20 18:08:28 +00:00
Ashley
83b52706f4 fix some issues :3 2023-01-20 10:22:02 +00:00
Ashley
699b91b60e add poketube flex to more stuff :3 2023-01-20 10:20:36 +00:00
Ashley
438e9dddf5 fix goofu issue~ 2023-01-20 07:01:42 +00:00
Ashley
cebaf648a6 add maximise!!! 2023-01-19 22:31:38 +00:00
Ashley
a0b7dfd322 add maximise!!! 2023-01-19 22:30:08 +00:00
Ashley
4304099260 bump version :3 2023-01-19 20:43:07 +00:00
Ashley
9ba1da8ae3 whoa cool stuff 2023-01-19 20:41:35 +00:00
Ashley
d0483d2c73 infinite looppppppppppppppppppppppppppp 2023-01-19 12:05:57 +00:00
Ashley
ecceaf09a6 more readable code :3 2023-01-18 11:28:46 +00:00
Ashley
a47d3070ad cool stuff~ 2023-01-18 11:26:48 +00:00
Ashley
6aa7a6e8a6 fix my issue ;_; 2023-01-18 09:07:28 +00:00
Ashley
007d3707f4 lmao flying comments 2023-01-17 21:06:38 +00:00
Ashley
2be1641a89 lmao flying comments 2023-01-17 21:04:29 +00:00
Ashley
a5bfe68c67 remove navbar buttons 2023-01-16 17:34:51 +00:00
Ashley
2d8f2a1390 remove proxy - for now 2023-01-14 08:05:49 +00:00
Ashley
b7d9219c36 fix url :p 2023-01-14 08:03:17 +00:00
Ashley
b9f5f7ad93 fix stuff 2023-01-14 08:01:40 +00:00
Ashley
206549b81c Merge pull request 'Some minor UI improvements' (#25) from fr33zing/poketube:arctic-ui into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/25
2023-01-14 07:26:23 +00:00
arctic
8e7eba1deb fix flag/text vertical alignment 2023-01-13 23:01:18 -08:00
arctic
9d1f7fcf48 less hacky fix for spacing + new variable 2023-01-13 22:46:26 -08:00
arctic
3200d1fa80 add class + move changes to landing.ejs 2023-01-13 22:08:00 -08:00
arctic
0589db85b4 fix landing button clickableness 2023-01-13 21:47:36 -08:00
arctic
a9860ac754 reduce spacing 2023-01-13 21:47:26 -08:00
arctic
04704ea46c fix indentation 2023-01-13 17:45:26 -08:00
arctic
6dc84131e3 remove commented style 2023-01-13 17:44:21 -08:00
arctic
cc62e2deab fix loggedout-dropdown button cursor 2023-01-13 17:32:13 -08:00
arctic
48daee2581 improve video-info-buttons spacing 2023-01-13 17:32:13 -08:00
arctic
ac18d3cf50 improve video-chnl-info-pill styles 2023-01-13 17:32:13 -08:00
Ashley
10cb93521e bump version :3 2023-01-13 15:44:33 +00:00
Ashley
96cbcc3fc3 add enable always https 2023-01-13 15:33:36 +00:00
Ashley
5901b013ee add hsts lol 2023-01-12 20:11:35 +00:00
Ashley
f4ff9848c8 add ukraine flag 2023-01-12 16:45:19 +00:00
Ashley
b387142d19 remove maxcdn - it shut down 2023-01-12 16:43:49 +00:00
Ashley
866ce9a23e add try catch :p 2023-01-12 16:14:31 +00:00
Ashley
f65c0f4d71 changw url lol 2023-01-12 15:59:09 +00:00
Ashley
15411b4742 change api url lool 2023-01-12 15:40:10 +00:00
Ashley
cbf1a1096d that that i like that 2023-01-11 19:31:47 +00:00
Ashley
a38608c2ca sethforprivacy :p 2023-01-11 19:18:19 +00:00
Ashley
496f7b7716 sethforprivacy :p 2023-01-11 19:15:42 +00:00
Ashley
d6137400d0 add non-Translated title :p 2023-01-11 19:14:02 +00:00
Ashley
dd1e9cdbbc wow cool stuff 2023-01-11 19:06:10 +00:00
Ashley
efec71476e Mystery text :3 2023-01-11 18:53:58 +00:00
Ashley
77b22fda86 bump dependencies 2023-01-11 16:39:01 +00:00
Ashley
2d6bb3589b lel 2023-01-11 16:32:47 +00:00
Ashley
5d9c0c3198 change copyright year 2023-01-11 16:32:18 +00:00
Ashley
56d25103a5 Merge pull request 'Docker Changes' (#23) from npgo22/poketube-docker:docker into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/23
2023-01-11 16:28:49 +00:00
Nolan Poe
a2d4091a38 Do not copy root on proxy dockerfile 2023-01-11 16:28:51 +00:00
Nolan Poe
b0d75415f3 Move files 2023-01-11 16:28:51 +00:00
Nolan Poe
81a5d8514f Docker Changes
- Add "release" and "release-aio" configurations based on alma linux
- Add nginx example and entrypoint script
- Dockerfile changes
- "p" was not removed
2023-01-11 16:28:50 +00:00
Ashley
f1e371ee01 change copyright year 2023-01-11 16:26:38 +00:00
Ashley
5d947aa62c add new api stuff :3 2023-01-11 16:24:54 +00:00
Ashley
451d206027 OK MY BAD 2023-01-11 16:23:54 +00:00
Ashley
d28bee1e7d add new api stuff :3 2023-01-11 16:20:38 +00:00
Ashley
174ca0fa3e add bad proxys 2023-01-10 19:03:11 +00:00
Ashley
a19111bb8f OK MY BAD 2023-01-10 16:18:22 +00:00
Ashley
af29e2e408 new proxy !!!!!!!!!!!!!!!!!!!!!!!! 2023-01-10 16:06:10 +00:00
Ashley
7ece06a3b3 new proxy !!!!!!!!!!!!! 2023-01-10 16:00:45 +00:00
Ashley
ba97cbf3ee new proxy !!!! 2023-01-10 15:59:43 +00:00
Ashley
3d09f8fbe5 yup, thats the commit 2023-01-09 21:23:29 +00:00
Ashley
4eb59acb1a bump version :3 2023-01-09 21:22:35 +00:00
Ashley
3edbe32ead TOO MANY COOKS 2023-01-09 21:16:26 +00:00
Ashley
d9b3acb142 nyaaaaaaaaaa tabs 2023-01-09 16:59:19 +00:00
Ashley
356aa8e409 fix some issues 2023-01-08 19:54:55 +00:00
Ashley
4cd6c33721 search bar fixes 2023-01-08 19:11:00 +00:00
Ashley
4b9a15eef4 im sowwy ;_; 2023-01-08 15:32:40 +00:00
Ashley
bd2b86433a literally unusable 2023-01-08 15:04:17 +00:00
Ashley
3063d4b5d6 landscape wow cool stuff 2023-01-08 14:55:26 +00:00
Ashley
0f510f500c font changes lel 2023-01-08 14:42:40 +00:00
Ashley
8ddf6f6827 max height for buttons 2023-01-08 14:41:25 +00:00
Ashley
81ae4651d5 new embed description :3 2023-01-08 12:21:44 +00:00
Ashley
07bcfbadd9 css improvments!! 2023-01-08 00:46:47 +00:00
Ashley
004cc7998b add try catch :p 2023-01-07 19:15:27 +00:00
Ashley
963a35145a make super init 2 mins 2023-01-07 19:08:01 +00:00
Ashley
63de51fae9 bump version :3 2023-01-07 16:28:31 +00:00
Ashley
43fe08eb0e change heights n stuff 2023-01-07 16:27:13 +00:00
Ashley
a69607f0a9 remove xmas logo :p 2023-01-07 16:13:31 +00:00
Ashley
395a8c5e72 remove snow :p 2023-01-07 16:11:02 +00:00
Ashley
7b8c7dcb73 add try catch :p 2023-01-07 14:52:46 +00:00
Ashley
96a8eba7ec new margin :3 2023-01-07 13:42:21 +00:00
Ashley
4bceb9f282 add more good video format :3 2023-01-07 13:39:46 +00:00
Ashley
a65f35a8ac use in ... data :p 2023-01-07 13:38:36 +00:00
Ashley
2b813618b6 Add stats for nerds :3 update copyright dates and make channel names more gooda owo 2023-01-07 01:25:58 +00:00
Ashley
548ebaeeb8 STATS FOR NERDS!!! 2023-01-07 01:24:36 +00:00
Ashley
00dcc547cf add lossless audio!! 2023-01-05 15:52:55 +00:00
Ashley
50409069b4 update music stuff :p 2023-01-05 15:49:55 +00:00
Ashley
1bdee30056 gaming 2023-01-05 15:43:55 +00:00
Ashley
f3011e4a35 change api url 2023-01-04 19:15:45 +00:00
Ashley
07b2a645ed add more urls :3 2023-01-02 20:05:11 +00:00
Ashley
ed1117096e yup, thats the commit 2023-01-02 18:07:55 +00:00
Ashley
aa898e312e make encryption unvisable 2023-01-02 18:05:22 +00:00
Ashley
2f259bf74f fix some silly issues :3 2023-01-02 18:04:15 +00:00
Ashley
2c6faf6f70 add some cool stuff! 2023-01-02 17:28:57 +00:00
Ashley
9087fdd2d8 improvements!! 2023-01-02 17:17:41 +00:00
Ashley
cd8919e672 fix href urls 2023-01-01 19:19:42 +00:00
Ashley
981e12b5b0 fix mobile search icon 2023-01-01 18:17:24 +00:00
Ashley
b42b76d5f7 add engagement class 2023-01-01 18:15:17 +00:00
Ashley
0e5f716184 add hover effects!! 2023-01-01 18:14:35 +00:00
Ashley
c8890f3af1 add poketube flex to more stuff :3 2023-01-01 16:23:07 +00:00
Ashley
72432c4e17 add rss feed!! :3 2023-01-01 10:00:04 +00:00
Ashley
760cdf8c94 add rss feed!'! 2023-01-01 09:59:18 +00:00
Ashley
2a457d586a remove view engine 2023-01-01 09:57:33 +00:00
Ashley
4c9886b03f add ambient mode 2.0 and lazy loading image 2022-12-31 20:13:26 +00:00
Ashley
badb096f6e add ambient mode 2.0 2022-12-31 20:12:12 +00:00
Ashley
c890eef61b add domains and privacy icon 2022-12-31 15:37:50 +00:00
Ashley
ac5e0a777b remove margins for navs :3 2022-12-31 15:36:52 +00:00
Ashley
f2e739e288 make it 1600px 2022-12-31 15:10:38 +00:00
Ashley
453ebffb2f add min-width :3 2022-12-31 14:57:37 +00:00
Ashley
6dad8a93d1 add margin values :3 2022-12-31 14:50:30 +00:00
Ashley
a4eff55a7a new font stuff 2022-12-31 14:17:05 +00:00
Ashley
241a0da0eb add dropdown menu :3 2022-12-31 14:09:52 +00:00
Ashley
392af71516 add dropdown menu 2022-12-31 14:07:55 +00:00
Ashley
da5daddf49 add try catch :p 2022-12-31 12:39:51 +00:00
Ashley
1aae2b4973 add docs url 2022-12-31 11:07:17 +00:00
Ashley
47096ec018 add a fix for searches 2022-12-31 11:05:26 +00:00
Ashley
40c549b2d0 so basically in germany you can eat food 2022-12-30 19:49:13 +00:00
Ashley
544169c40e fix a issue about search :3 2022-12-30 16:22:29 +00:00
Ashley
104eb86429 add Switch instance link 2022-12-30 16:19:26 +00:00
Ashley
eb64479e5c fix my issue ;_; 2022-12-30 13:59:49 +00:00
Ashley
1b4dfc36cc bump version :3 2022-12-29 21:06:03 +00:00
Ashley
42ee7dc46c change timeout to warming up 2022-12-29 20:30:21 +00:00
Ashley
7441da69c4 new description :3 2022-12-29 20:28:56 +00:00
Ashley
0b2624132d new description :3 2022-12-29 20:27:52 +00:00
Ashley
c80d2e45dd yes 2022-12-29 20:25:50 +00:00
Ashley
1fde25c086 change inv url 2022-12-29 16:31:28 +00:00
Ashley
bac8b31405 fix & lol 2022-12-29 15:47:26 +00:00
Ashley
d0c57e8f09 add more liks 2022-12-29 15:38:44 +00:00
Ashley
ccba3c86b5 christmas logo 2022-12-29 14:46:01 +00:00
Ashley
5ca7dc84ef add credits easter egg owo! 2022-12-26 23:54:55 +00:00
Ashley
5a65f36e91 add credits easter egg :3 2022-12-26 23:54:21 +00:00
Ashley
a2635c5b46 add credits easter egg 2022-12-26 23:53:43 +00:00
Ashley
fa0a945c72 add orange logo 2022-12-26 23:50:53 +00:00
Ashley
4fc2eea6a6 add " " 2022-12-25 20:56:33 +00:00
Ashley
4c1d657210 re-add vern.cc 2022-12-25 20:51:31 +00:00
Ashley
716f3f4631 re-add vern.cc 2022-12-25 20:50:31 +00:00
Ashley
d0f1ba62a7 re-add vern.cc 2022-12-25 20:49:25 +00:00
Ashley
51330a14b0 bump version :3
Signed-off-by: Ashley <iamashley@duck.com>
2022-12-25 16:43:06 +00:00
Ashley
8446919b11 use the in operator lol 2022-12-25 16:42:08 +00:00
Ashley
d504b848bd add secure instance icon !! 2022-12-25 16:22:20 +00:00
Ashley
9bbaecd268 add secure instance icon !! 2022-12-25 16:12:56 +00:00
Ashley
82858e7d58 add secure instance icon !! 2022-12-25 16:10:51 +00:00
Ashley
6d8c5d6006 new version beep boop boop 2022-12-24 19:36:26 +00:00
Ashley
9485f1d0c2 new timeout page :3 2022-12-24 19:34:26 +00:00
Ashley
7ac4105844 check if formats exist 2022-12-24 19:27:24 +00:00
Ashley
3210066cb1 add warning for non offical instances 2022-12-24 19:02:58 +00:00
Ashley
ea835f8a3b add official checks 2022-12-24 18:19:54 +00:00
Ashley
52a4bd4de1 Merge pull request 'Update 'instances.json'' (#21) from exhq/poketube:main into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/21
2022-12-24 17:52:44 +00:00
exhq
8e3e9550c9 Update 'instances.json' 2022-12-24 17:46:54 +00:00
Ashley
2a3eeb9f5b sjohgteojgytrueugtye4jhtytjrjnyıı 2022-12-24 12:59:34 +00:00
Ashley
57b5ae0dde fix the name of the file 2022-12-24 12:53:07 +00:00
Ashley
a595d57e62 ADD poketube color pallate 2022-12-24 12:43:06 +00:00
Ashley
bc654ff5be bump version :3 2022-12-24 12:31:24 +00:00
Ashley
3f7bd826f5 add more redirects 2022-12-24 12:30:31 +00:00
Ashley
ecac0323b7 fix "TypeError: Cannot use 'in' operator to search for 'Title' in undefined" 2022-12-24 12:28:41 +00:00
Ashley
ab1bb9bd45 add a check if json variable exists or not
Signed-off-by: Ashley <iamashley@duck.com>
2022-12-24 11:30:00 +00:00
Ashley
29cf785969 bump version :3 2022-12-24 10:53:05 +00:00
Ashley
d21547c5ab format some code 2022-12-24 10:51:44 +00:00
Ashley
99d4b80f43 also remove yandere code :3 2022-12-24 10:49:52 +00:00
Ashley
eb41e742d3 remove yandere code :3 2022-12-24 10:48:55 +00:00
Ashley
653625c06f add some spaces :3 2022-12-24 10:48:00 +00:00
Ashley
3085c5e5b9 new css files for mobile 2022-12-23 11:38:49 +00:00
Ashley
11a429e449 add tags to mobile and new mobile ui :3 2022-12-23 11:37:01 +00:00
Ashley
7975435277 add try catch :p 2022-12-21 15:42:06 +00:00
Ashley
8d613c2652 add try and catch to about section 2022-12-21 15:38:26 +00:00
Ashley
1f9f5aaf6f add try and catch to about section 2022-12-21 08:25:16 +00:00
Ashley
cfb0176d21 add description check :p 2022-12-20 20:19:02 +00:00
Ashley
f129b60395 fix isvalidvideo - gonna check if this is valid 2022-12-20 17:16:22 +00:00
Ashley
51667dc3db remove desc :p 2022-12-20 15:52:59 +00:00
Ashley
d7bca3440a add channel about checker 2022-12-20 14:42:07 +00:00
Ashley
53f57c6354 improve return youtube dislike API 2022-12-20 13:38:25 +00:00
Ashley
0dfe5317af fix RYD api 2022-12-20 11:52:34 +00:00
Ashley
8013a7b56a bump version 2022-12-20 11:03:24 +00:00
Ashley
d302e4291d fix encryption owo 2022-12-20 11:01:50 +00:00
Ashley
c133288d37 fix dislikes lel 2022-12-20 10:59:28 +00:00
Ashley
b69e87393d use getJson instead of json.parse 2022-12-20 10:57:23 +00:00
Ashley
9281866018 fix some issues :p 2022-12-20 08:21:13 +00:00
Ashley
e016ae062a fix urls :p 2022-12-20 07:58:57 +00:00
Ashley
fb88ff227b Merge pull request 'several enhancements related to proxy microservice' (#20) from janderedev/poketube:main into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/20
2022-12-20 07:48:29 +00:00
Ashley
38134bb908 remove old video downloader 2022-12-20 07:46:46 +00:00
Lea
e6e19e4ee2 chore: remove unneeded imports 2022-12-19 19:12:23 +01:00
Lea
1e8090e661 fix: fixed several issues with proxy
- proxy no longer crashes with malformed input
- use URL whitelist instead of blindly proxying everything
- clean up code
2022-12-19 18:22:43 +01:00
Lea
7c8e99c604 feat: dockerize image proxy 2022-12-19 18:05:16 +01:00
Ashley
efd36eb9f5 fix a ,ssıe p 2022-12-19 15:54:30 +00:00
Ashley
124147361d bump version 2022-12-19 14:30:34 +00:00
Ashley
5cf6aeb0e7 libpoketube 2.1 :3 2022-12-19 14:29:33 +00:00
Ashley
03115c7f3c check if object is null 2022-12-19 14:28:09 +00:00
Ashley
5b168c1ede reformat file :p 2022-12-19 11:52:57 +00:00
Ashley
d0583af7f5 add try catch :p 2022-12-18 15:30:24 +00:00
Ashley
24d21fda9d add margin values :3 2022-12-18 15:15:26 +00:00
Ashley
d2291d7678 im sowwy ;_; 2022-12-18 15:02:03 +00:00
Ashley
4763dd18d8 add != "assets" 2022-12-18 14:11:24 +00:00
Ashley
bf575e0340 make it 20minutes lel 2022-12-18 14:03:19 +00:00
Ashley
9d435daf55 make it 20minutes lel 2022-12-18 14:02:28 +00:00
Ashley
57567fb124 fix somethin lel 2022-12-18 14:00:51 +00:00
Ashley
5d50ad0fb0 add regex lol 2022-12-18 11:56:33 +00:00
Ashley
3c056ce243 remove old video downloader 2022-12-18 11:21:48 +00:00
Ashley
1b97c2a7bb make super init 16 mins 2022-12-18 11:01:38 +00:00
Ashley
d5d8704b63 make timeout 16 minutes 2022-12-18 11:00:46 +00:00
Ashley
59c685a27b center video grid 2022-12-17 08:38:01 +00:00
Ashley
1cbb9d29df fix somethin lel 2022-12-16 22:18:03 +00:00
Ashley
349cc76dfa add somethin to check if data video 2022-12-16 19:16:32 +00:00
Ashley
b08b73ae52 checkUnexistingObject lel 2022-12-16 19:11:53 +00:00
Ashley
edc74ce59f add title lel 2022-12-16 15:06:12 +00:00
Ashley
8e60f40ba0 new watch page widths!! 2022-12-16 14:44:57 +00:00
Ashley
6dc8b575d7 bump version 2022-12-15 16:26:25 +00:00
Ashley
31f734f264 fix a silly issy 2022-12-15 16:22:03 +00:00
Ashley
54e7702614 fix download page :p 2022-12-15 16:20:03 +00:00
Ashley
ac7771abd2 fix download page :p 2022-12-15 16:16:08 +00:00
Ashley
685e25ac1b bump version 2022-12-15 16:08:10 +00:00
Ashley
e4709fe1dc qwq 2022-12-15 16:07:03 +00:00
Ashley
58ae9094cd new timeout page 2022-12-15 16:02:58 +00:00
Ashley
ec84b600f4 add redirects to results 2022-12-15 15:53:01 +00:00
Ashley
5ea6a7cdb8 fix somethin lel 2022-12-15 15:07:28 +00:00
Ashley
efbb16093a ad media query :p 2022-12-15 14:31:55 +00:00
Ashley
57ec0d25e3 test run - new max width for watch-page 2022-12-14 20:37:00 +00:00
Ashley
8a58a75919 display flex lel 2022-12-14 19:21:58 +00:00
Ashley
982f9eac4f reformat file - fix SD videos 2022-12-14 19:03:30 +00:00
Ashley
43f26f53db thanks chrome really cool 2022-12-14 16:58:30 +00:00
Ashley
82642a8c84 add watch-page grid 2022-12-14 16:54:38 +00:00
Ashley
3ba5fc4cd0 [temp] this is for now lol 2022-12-14 16:40:16 +00:00
Ashley
20d2051a32 add some fixes - i hope this would be last 2022-12-14 15:58:21 +00:00
Ashley
9508872454 ok im sorry 2022-12-14 15:34:38 +00:00
Ashley
5218966d29 make it better :p 2022-12-14 15:32:18 +00:00
Ashley
6b6482087a new video player!! 2022-12-14 15:16:17 +00:00
Ashley
ececa444bf remove vern.cc 2022-12-13 18:42:21 +00:00
Ashley
fac54c14bb ok im sosr 2022-12-13 18:32:42 +00:00
Ashley
aeb2d91d94 fix a silly issy 2022-12-13 18:30:51 +00:00
Ashley
bdf481239e fixu apii!! 2022-12-13 18:24:28 +00:00
Ashley
3201717036 fix api lel 2022-12-13 18:23:38 +00:00
Ashley
88b697a4b8 fixu apii!! 2022-12-13 18:22:51 +00:00
Ashley
c6a644be35 fix api!!! 2022-12-13 18:22:15 +00:00
Ashley
b8fecc8882 fix api 2022-12-13 18:21:27 +00:00
Ashley
322542c38e new logo!!!! 2022-12-12 18:22:07 +00:00
Ashley
fea276026f add -moz- to fit content 2022-12-10 19:58:54 +00:00
Ashley
f9cc3d000c remove snow 2022-12-10 11:11:21 +00:00
Ashley
6c11f2746a fix somethin lel 2022-12-10 11:08:15 +00:00
Ashley
d16fab17a7 fix somethin lel 2022-12-10 08:07:03 +00:00
Ashley
1379e547c1 community tabss!!! 2022-12-10 07:47:02 +00:00
Ashley
212aa50bbf community tabss!!! 2022-12-10 07:46:23 +00:00
Ashley
9e131fd7dd mobile improvements!! 2022-12-10 05:51:54 +00:00
Ashley
03f0ce5453 mobile improvements!! 2022-12-09 21:55:22 +00:00
Ashley
85dc34686d fix mobile stuff 2022-12-09 21:53:28 +00:00
Ashley
76ccb074b1 make cooldown 50 secs 2022-12-09 20:36:45 +00:00
Ashley
ea38b3fd3e fix buyyons lol 2022-12-09 20:34:46 +00:00
Ashley
c8ec63b903 fix margin lel 2022-12-09 19:13:50 +00:00
Ashley
c371baed17 fix texts 2022-12-09 19:10:26 +00:00
Ashley
7b823cacd4 fix dislikes - again 2022-12-09 19:06:19 +00:00
Ashley
2025f2c50a mobile improvements!! 2022-12-09 19:05:09 +00:00
Ashley
120f2d52c9 fix dislikes 2022-12-09 19:03:28 +00:00
Ashley
684a144ec9 fix channels!!!!!! 2022-12-09 17:13:14 +00:00
Ashley
2c1b66ee72 fix channels!!!!!! 2022-12-09 17:12:21 +00:00
Ashley
19de1e7690 make margin lower 2022-12-09 12:04:25 +00:00
Ashley
99582b1a55 version 100!!! 2022-12-09 11:39:49 +00:00
Ashley
03ae29606a fix some stuff 2022-12-09 11:38:20 +00:00
Ashley
c5188025c4 add mobile.css 2022-12-09 11:37:25 +00:00
Ashley
14e73d7414 add nowrap 2022-12-09 11:36:46 +00:00
Ashley
03caf4c3d9 new mobile stuff :p 2022-12-08 21:06:36 +00:00
Ashley
18a1879364 add border to thumbs 2022-12-08 18:27:48 +00:00
Ashley
f65c05476b add better lyrics!! 2022-12-08 18:26:04 +00:00
Ashley
3316a3e8a3 add snow and imrpove comments!! 2022-12-08 18:15:37 +00:00
Ashley
085cf3f2f9 version bump :3 2022-12-05 17:48:50 +00:00
Ashley
bcc440db31 Update 'server.js' 2022-12-05 17:44:37 +00:00
Ashley
0566610b26 Add 'html/timeout.ejs' 2022-12-05 17:44:10 +00:00
Ashley
171aa3a552 add timeout :3 2022-12-05 17:43:26 +00:00
Ashley
bbfc6d4254 qwq 2022-12-04 13:17:36 +00:00
Ashley
74beee01b6 qwq 2022-12-04 12:26:17 +00:00
Ashley
e2b81091ce margin-left:24px; 2022-12-04 12:24:43 +00:00
Ashley
c9a076c81f qwq 2022-12-04 12:23:50 +00:00
Ashley
e838203f6b better responsive!!! 2022-12-04 11:48:19 +00:00
Ashley
8e7bb7a620 Delete 'css/poketube-responsive.css' 2022-12-04 11:46:15 +00:00
Ashley
7445b7da33 fixes :3 2022-12-04 11:45:47 +00:00
Ashley
c3c0f80471 fixes :3 2022-12-04 11:45:05 +00:00
Ashley
f11617d01f fix widths :p 2022-12-03 23:12:58 +00:00
Ashley
94f6372be5 improve responsive mode :3 2022-12-03 23:12:04 +00:00
Ashley
6359248972 Improvements owowowowowo 2022-12-03 23:05:07 +00:00
Ashley
bb75fb9672 margin for 2000px :> 2022-12-03 17:52:35 +00:00
Ashley
1387720a32 variable 2022-12-03 11:37:44 +00:00
Ashley
ec2167d917 variable 2022-12-03 11:36:33 +00:00
Ashley
d31ce44c9d 4px margin lel 2022-12-01 20:47:49 +00:00
Ashley
1f37c895b3 new classes 2022-12-01 19:22:22 +00:00
Ashley
8f92f1affd remove lite from music videos 2022-12-01 17:21:20 +00:00
Ashley
42d24de4b4 fix widths :p 2022-12-01 17:20:04 +00:00
Ashley
2f45bc7a03 qwq 2022-12-01 15:59:31 +00:00
Ashley
4983b2a3ba fix a silly issue :3 2022-12-01 15:56:48 +00:00
Ashley
eb6f1e2383 bump version :3 2022-11-30 18:51:35 +00:00
Ashley
4e1154cfbf ADD LITE!!!! 2022-11-30 18:49:09 +00:00
Ashley
a9946aa831 POKETUBE LITE!!!! HELL YEAH!!!! 2022-11-30 18:48:15 +00:00
Ashley
76e9f3182e go lite! hell yeah
POG
2022-11-30 18:44:53 +00:00
Ashley
3a2b5c16e1 remove scrollbar 2022-11-30 14:38:09 +00:00
Ashley
87cdb052d9 add snow!!! 2022-11-30 14:35:13 +00:00
Ashley
3c57b8547a add snow!!! 2022-11-30 14:34:17 +00:00
Ashley
9d20c6b279 fix scroll on chromium 2022-11-30 14:03:10 +00:00
Ashley
ec99b8449f bump version :3 2022-11-29 21:28:38 +00:00
Ashley
e6a8d9ec36 margin imprv :3 2022-11-29 21:26:50 +00:00
Ashley
842a4f774f new title!! 2022-11-29 21:25:31 +00:00
Ashley
4d9151a30a imporv 2022-11-29 17:56:21 +00:00
Ashley
a295f61681 margin fixesssssssss :3 2022-11-27 20:04:35 +00:00
Ashley
ba57f46725 margin fixesssssssss :3 2022-11-27 20:03:06 +00:00
Ashley
3c7229a017 improve responsive mode :3 2022-11-26 19:41:17 +00:00
Ashley
44b34ff3c8 5.5px :p 2022-11-26 19:39:48 +00:00
Ashley
d6671219dc .7 2022-11-26 17:48:55 +00:00
Ashley
2f60da94cf 55em lol 2022-11-26 17:45:45 +00:00
Ashley
06ce4be80c bump version :3 2022-11-26 17:03:35 +00:00
Ashley
e14160ce37 add left margin :3 2022-11-26 16:05:20 +00:00
Ashley
f336177cba fix some issues :3 2022-11-26 10:35:46 +00:00
Ashley
3ae35fd3bf remove non-proxied conten 2022-11-25 15:53:58 +00:00
Ashley
322cb92dc3 Mobile Improvements :3 2022-11-25 15:24:25 +00:00
Ashley
6f17c21087 add ambien mode to mobile 2022-11-24 21:00:00 +00:00
Ashley
f911d3af08 NEW LAYOUUUUUUUUT 2022-11-24 20:29:18 +00:00
Ashley
10d15e6aed new layout!!!!!!!!!! 2022-11-24 20:28:18 +00:00
Ashley
9ee568a54f fix re3ws, 2022-11-24 20:27:30 +00:00
Ashley
f19b21e263 lightOrDark :3 2022-11-24 20:26:41 +00:00
Ashley
46d8827a09 add increase_brightness 2022-11-24 20:14:36 +00:00
Ashley
42b4ddc948 add second color 2022-11-24 20:13:32 +00:00
Ashley
101d38175e fix a silly issue :3 2022-11-23 16:45:45 +00:00
Ashley
23f4616926 improve responsive mode :3 2022-11-23 16:33:25 +00:00
Ashley
c8b6164293 tags!!!!! 2022-11-21 16:31:15 +00:00
Ashley
244e708edd tags!!!!! 2022-11-21 16:17:50 +00:00
Ashley
aa222cd19f improvements to poketube html :3
Signed-off-by: Ashley <iamashley@duck.com>
2022-11-20 15:00:54 +00:00
Ashley
63b76865a4 fix some issues :3 2022-11-20 13:46:06 +00:00
Ashley
2bd44d1b84 remove pt flex from watch-util 2022-11-20 08:54:21 +00:00
Ashley
1f701d0eb5 add font-weight 2022-11-20 08:48:22 +00:00
Ashley
bc7ded381f fix some css issues :p 2022-11-20 08:46:15 +00:00
Ashley
92e89eeca3 fix some issues :3 2022-11-19 18:06:33 +00:00
Ashley
9c6825818d Final change, promise 2022-11-18 12:08:49 +00:00
Ashley
963386f59d still WIP! 2022-11-18 12:05:56 +00:00
Ashley
8ebca9a212 fix a silly issue :3 2022-11-18 11:39:21 +00:00
Ashley
5070f9cbef put Results at bottom - still WIP! 2022-11-18 11:38:13 +00:00
Ashley
0395fbb152 new search page !!!! 2022-11-18 11:14:48 +00:00
Ashley
2a9eeb693e bump version :3 2022-11-18 09:53:13 +00:00
Ashley
f363040746 add did you mean? prompt 2022-11-18 09:52:35 +00:00
Ashley
8f49fec0cb fix a silly issue :3 2022-11-18 09:47:25 +00:00
Ashley
e3cb7256ee add did you mean? prompt 2022-11-18 09:42:25 +00:00
Ashley
956df8e5cf fix a silly issue :3 2022-11-16 18:32:02 +01:00
Ashley
fd5315df7f check if title is there 2022-11-16 17:59:03 +01:00
Ashley
8a9f01cab8 use getjson :3 2022-11-16 17:58:03 +01:00
Ashley
ae52a0da00 bump version :3 2022-11-16 17:44:49 +01:00
Ashley
14e770fe4c faster fetching :3 2022-11-16 17:43:53 +01:00
Ashley
4d87c3621d faster fetching :3 2022-11-16 17:42:06 +01:00
Ashley
44a938852d bump version :3 2022-11-16 15:39:15 +01:00
Ashley
c972e84a55 we did it reddit! theres now non-array subtitle support!!! 2022-11-16 15:37:26 +01:00
Ashley
f6e0e6667a bump version :3 2022-11-16 15:00:15 +01:00
Ashley
416ced03ec fix a silly issue :3 2022-11-16 14:59:30 +01:00
Ashley
a615ed855c add version numbers 2022-11-16 14:56:26 +01:00
Ashley
5118fe03f9 Remake channel pages - not working on mobile for now 2022-11-16 14:49:42 +01:00
Ashley
cfdb907270 update thignys 2022-11-16 11:45:08 +01:00
Ashley
2286309f12 bump version :3 2022-11-16 11:35:34 +01:00
Ashley
d74796f09c bump dependencies 2022-11-16 11:32:24 +01:00
Ashley
374d77970a add version deamons 2022-11-16 11:27:48 +01:00
Ashley
e3f15542b1 add update daemon 2022-11-16 11:26:47 +01:00
Ashley
14ccdd53b8 catch errors :^ 2022-11-15 17:45:01 +01:00
Ashley
0fa9f3dbf6 add /version!!! 2022-11-15 17:40:40 +01:00
Ashley
d65a6f55d4 remove maximum line :^ 2022-11-15 16:32:37 +01:00
Ashley
cf0eee3340 add a .4 to 1300x px devices
This commit was Signed by:

Signed-off-by: Ashley <iamashley@duck.com>
2022-11-15 16:22:07 +01:00
Ashley
8327bccfe3 new search form :3 2022-11-15 13:06:34 +01:00
Ashley
f9c77f8b31 new search form :3 2022-11-15 12:58:09 +01:00
Ashley
923b849276 add word break :p 2022-11-15 01:56:59 +01:00
Ashley
5af26d1802 add a easter egg :p 2022-11-14 23:43:30 +01:00
Ashley
47a8996d5f fix a silly css issue :p 2022-11-14 23:40:52 +01:00
Ashley
fc2d509a01 improve responsive mode :3
Signed-off-by: Ashley <iamashley@duck.com>
2022-11-14 21:45:33 +01:00
Ashley
44774bb194 improve the info panel
- Add Maximum Line width
- Add auto wrap

Signed-off-by: Ashley <iamashley@duck.com>
2022-11-14 21:44:24 +01:00
Ashley
b5d2d4515d Improvements owowowowowo 2022-11-14 21:34:06 +01:00
Ashley
8d8a472cc5 add support for 2560px screens
Signed-off-by: Ashley <iamashley@duck.com>
2022-11-14 18:38:35 +01:00
Ashley
43acfe3352 refactor code :3 2022-11-14 18:37:20 +01:00
Ashley
b60e06a9cd Refactor code, add more gooda error thingy :3 2022-11-14 18:36:00 +01:00
Ashley
05c9ba9304 fix some issues :3 2022-11-14 17:33:55 +01:00
Ashley
259e1485f2 add poketube responsive file :3 2022-11-14 17:33:20 +01:00
Ashley
6b2d215247 add responsive stuff to another file 2022-11-14 17:31:44 +01:00
Ashley
67d7e16cd7 add responsğve stuff to another file 2022-11-14 17:30:49 +01:00
Ashley
dec253fd19 add /shorts 2022-11-13 14:00:40 +01:00
Ashley
1094e80c5f PokeTube Flex in more places :3 2022-11-13 13:59:24 +01:00
Ashley
6d1907e4fb qwq 2022-11-13 13:58:20 +01:00
Ashley
5890ff1eec fix some issues :3 2022-11-13 12:49:48 +01:00
Ashley
3e9ec2b6a0 qwq 2022-11-13 11:06:29 +01:00
Ashley
1ed19aec17 qwq 2022-11-12 22:31:48 +01:00
Ashley
f5faae4ddc Improvements owowowowowo 2022-11-12 22:30:52 +01:00
Ashley
bd302a5a62 qwq 2022-11-12 13:51:50 +01:00
Ashley
ef3ccb908f responsive stuff~ 2022-11-12 10:48:48 +01:00
Ashley
594da80994 new mobile layout :3 2022-11-11 21:46:41 +01:00
Ashley
83b2935d17 Improvements owowowowowo 2022-11-11 12:15:39 +01:00
Ashley
a18636dfa1 remove ginto nord :3 2022-11-11 11:54:31 +01:00
Ashley
244e9a1053 Merge pull request 'pwease ashley-chan add my cutey wutey eashteh eww >w<' (#16) from exhq/poketube:main into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/16
2022-11-10 14:34:01 +01:00
Ashley
9636794ac2 fix a silly issue :3 2022-11-10 14:27:26 +01:00
echo
83cf3bb62d :D 2022-11-10 09:08:32 +03:30
Ashley
605c0b1079 Superinit!!! 2022-11-09 17:54:31 +01:00
Ashley
e6b8ca0254 Superinit!!! 2022-11-09 17:54:00 +01:00
Ashley
1bd2b5d075 Superinit!!! 2022-11-09 17:53:26 +01:00
Ashley
298b89f31a Superinit!!! 2022-11-09 17:52:57 +01:00
Ashley
638eabcec4 fix syntax issue :p 2022-11-09 17:52:08 +01:00
Ashley
ddee8f9c79 Superinit!!! 2022-11-09 17:51:10 +01:00
Ashley
2bb00d3c9f Superinit!!! 2022-11-09 17:48:29 +01:00
Ashley
f0f1a12db3 Superinit!!! 2022-11-09 17:47:46 +01:00
Ashley
4533880104 Superinit!!! 2022-11-09 17:46:37 +01:00
Ashley
1c8ca99160 libpoketube 2.0! 2022-11-09 15:27:56 +01:00
Ashley
fddb34e4aa libpoketube 2.0! 2022-11-09 15:17:03 +01:00
Ashley
3f8ceb8351 qwq 2022-11-08 21:24:55 +01:00
Ashley
4f1e85ce30 add search icon :3 2022-11-08 21:21:07 +01:00
Ashley
f6cdf6ffc8 add search icon :3 2022-11-08 21:20:31 +01:00
Ashley
ee1ae687b5 add standwithukraine 2022-11-08 20:11:39 +01:00
Ashley
a6b0e0e1cc qwq 2022-11-08 17:55:46 +01:00
Ashley
d905150463 new discover page :3 2022-11-08 17:10:31 +01:00
Ashley
cdc6cfc243 new landing !!!!!!111 2022-11-08 17:08:19 +01:00
Ashley
6aec4cabaf new filename :3 2022-11-08 17:07:17 +01:00
Ashley
ceeedf38dc new landing bg :3 2022-11-08 16:38:33 +01:00
Ashley
52314fc32c dark logo :3 2022-11-08 16:37:34 +01:00
Ashley
1f66f95bc5 Merge pull request 'a new instance? owo' (#15) from sylentpunk/poketube:main into main
Reviewed-on: https://codeberg.org/Ashley/poketube/pulls/15
2022-11-08 16:36:39 +01:00
Ashley
6b6510883c :p 2022-11-08 16:35:38 +01:00
sylentpunk
a0242cbc39 a new instance? owo
added poketube.sylentpunk.xyz and fixed some spelling
thank u for ur hard work and the help with setting it up ashley qt
2022-11-08 06:39:37 +01:00
Ashley
dc4d6dad6b :p 2022-11-07 21:09:01 +01:00
Ashley
2781b0b209 fix url :p 2022-11-07 21:08:07 +01:00
Ashley
6f5f580777 remove try catch 2022-11-07 21:07:18 +01:00
Ashley
016b89830b qwq 2022-11-07 20:59:05 +01:00
Ashley
3f192a16fb shortu urls 2022-11-07 20:55:05 +01:00
Ashley
04898a9205 Improvements owowowowowo 2022-11-07 20:33:46 +01:00
Ashley
4952705e07 qwq 2022-11-07 19:37:59 +01:00
Ashley
2a789379d8 reformat file :3 2022-11-07 18:58:27 +01:00
Ashley
e502e71659 Improvements owowowowowo 2022-11-07 18:54:47 +01:00
Ashley
1b60dcfc9c qwq 2022-11-07 17:53:58 +01:00
Ashley
e460c94a18 qwq 2022-11-07 17:52:40 +01:00
Ashley
5634a5cb87 Improvements owowowowowo 2022-11-07 17:49:21 +01:00
Ashley
11da1178d4 i hate css 2022-11-07 14:47:23 +01:00
Ashley
125f59ad5f fixu css 2022-11-07 05:15:49 +01:00
Ashley
c4c3457c09 Improvements owowowowowo 2022-11-07 05:12:51 +01:00
Ashley
9997aaa4fa fix a silly css issue 2022-11-06 22:07:59 +01:00
Ashley
900f7289f1 qwq 2022-11-06 21:28:07 +01:00
Ashley
db2900088a fix channels 2022-11-06 16:14:39 +01:00
Ashley
1931afd8df fix api 2022-11-06 16:14:03 +01:00
Ashley
863bc52f41 font to desc :^ 2022-11-06 15:47:23 +01:00
Ashley
35ac98cc9c Mobile Improvements :3 2022-11-06 15:06:55 +01:00
Ashley
b356e24d40 fix some issues :3 2022-11-06 14:24:37 +01:00
Ashley
1c8def460d new embeds :3 2022-11-06 14:14:44 +01:00
Ashley
beca0a1bce new embed!!!! 2022-11-06 14:14:01 +01:00
Ashley
3edcd67ec6 new embed!!!! 2022-11-06 14:13:16 +01:00
Ashley
1de820ca48 add 143 easter egg 2022-11-06 12:09:28 +01:00
Ashley
fe6009c26d add new core util functions 2022-11-06 12:08:12 +01:00
Ashley
92cb387ced new sub button :3 2022-11-06 12:01:40 +01:00
Ashley
d042dec13f Improvements owowowowowo 2022-11-06 12:00:46 +01:00
Ashley
46ea13a01b remove ginto nord :3 2022-11-06 11:36:01 +01:00
Ashley
4b5d430694 PokeTube Flex in more places :3 2022-11-06 11:26:20 +01:00
Ashley
2d446fe86c Improvements owowowowowo 2022-11-06 11:25:18 +01:00
Ashley
eae965842a justify content center 2022-11-05 19:55:14 +01:00
Ashley
c55ad95ca9 lmao please work this time or i will kill h
yeah so basically im tired at this point to edit this file lmao

PLEASE CSS GODS HELP ME
2022-11-05 16:42:45 +01:00
Ashley
39ad3a161a qwq 2022-11-05 15:45:25 +01:00
Ashley
bb373e9aef new layout for main menu!!1 2022-11-05 13:59:44 +01:00
Ashley
84737347f4 gib back views 2022-11-05 13:58:20 +01:00
Ashley
99b542c0ae video-views class 2022-11-05 13:27:18 +01:00
Ashley
15407569f2 new stuff :3 2022-11-05 13:21:09 +01:00
Ashley
9b7975c38d new video lenght!!! 2022-11-05 12:57:44 +01:00
Ashley
a6e667d224 fix centered issue 2022-11-05 12:49:15 +01:00
Ashley
36409c6583 video-length :3 2022-11-05 12:36:35 +01:00
Ashley
c6c0ff355d new css :3 2022-11-05 10:37:34 +01:00
Ashley
4bf27ce8c1 add poketube.css 2022-11-05 10:33:43 +01:00
Ashley
743f79d54a lmao 2022-11-03 21:34:37 +01:00
Ashley
e2aa3025b0 Improvements owowowowowo 2022-11-03 21:24:00 +01:00
Ashley
4a145a30a2 something something width
aww man
2022-11-03 20:56:03 +01:00
Ashley
35d90e0e5d MOBILE OPTIMIZED SEARCHHHHHHHH 2022-11-03 18:45:29 +01:00
Ashley
148175c18d mobile search pog 2022-11-03 18:44:29 +01:00
Ashley
af5c225976 add warnings 2022-11-03 17:44:30 +01:00
Ashley
118e43bb7e Improvements owowowowowo 2022-11-03 17:43:47 +01:00
Ashley
c3002533a6 Improvements owowowowowo 2022-11-02 19:16:21 +01:00
Ashley
bca69c67e7 fix some issues :3 2022-11-01 21:42:59 +01:00
Ashley
ee76602179 NEW GRADIENTSSS 2022-11-01 21:29:36 +01:00
Ashley
9ddae2897e FIX MY SKILL ISSUEEE 2022-11-01 19:01:17 +01:00
Ashley
34ae6d236f new layout for main menu!!1 2022-11-01 18:58:12 +01:00
Ashley
bd65bcb413 new stuff :3 2022-11-01 16:47:27 +01:00
Ashley
41a147a659 new layout :^ (BETA) 2022-10-29 15:07:11 +02:00
Ashley
333a56e627 change to new libpoketube version 2022-10-29 09:50:39 +02:00
Ashley
07cebfe449 add .modules for node-modules 2022-10-29 09:49:18 +02:00
Ashley
21083a9cf4 fix formatting 2022-10-28 22:39:15 +02:00
Ashley
f579f8fc4e make everything cleaner 2022-10-28 22:37:34 +02:00
Ashley
29ee88099d make it more clear 2022-10-28 21:59:37 +02:00
Ashley
fd6eaa8339 Add Additional Terms for poketube 2022-10-28 21:51:43 +02:00
Ashley
3ba3567908 Add 'CODE_OF_CONDUCT.md' 2022-10-28 21:38:41 +02:00
Ashley
bd8ae23195 new readme pog!!! thanks to https://github.com/htmlpaws :3 2022-10-28 17:46:29 +02:00
Ashley
98ec544c07 switch inv istance 2022-10-28 17:34:08 +02:00
Ashley
b0e97f4e2a FIX A BUG 2022-10-28 17:32:44 +02:00
Ashley
f5111a45d3 libpoketube version 1.1! 2022-10-28 16:47:37 +02:00
Ashley
9d64d2e8f3 add libpoketube loader 2022-10-28 16:46:35 +02:00
Ashley
cfda8fa255 add libpoketube core utils 2022-10-28 16:45:39 +02:00
Ashley
0c3db7d881 add public inv api :3 2022-10-27 17:42:38 +02:00
Ashley
6491e18098 fix some issues :3 2022-10-27 17:28:50 +02:00
Ashley
fbf9154f96 fix a silly div issue lmao 2022-10-27 17:06:45 +02:00
Ashley
97879843d0 add libpoketube to main file 2022-10-27 11:32:32 +02:00
Ashley
a82c2d681f libpoketube - fetcher 2022-10-27 11:31:39 +02:00
Ashley
f0c0981dbc libpoketube ! 2022-10-27 11:30:49 +02:00
Ashley
d85e6efb38 fix some issues on the search smart descs 2022-10-25 21:54:53 +02:00
Ashley
2e5fe0574b update thignys 2022-10-25 17:53:27 +02:00
Ashley
656ac3412a fix some issues :3 2022-10-24 17:56:10 +02:00
Ashley
7d73b24b3e fix some issues :3 2022-10-23 20:00:08 +02:00
Ashley
c99a7f0166 add comment previews 2022-10-23 19:28:39 +02:00
Ashley
ed2d166a02 Mobile Improvements :3 2022-10-23 18:02:52 +02:00
Ashley
82e9fcbf30 Comment Improvements v2 pog 2022-10-22 17:00:30 +02:00
Ashley
2ca8d57c15 Comment Improvements v2 pog 2022-10-22 16:58:54 +02:00
Ashley
54137b8170 Comment Improvements v2 pog 2022-10-22 16:58:07 +02:00
Ashley
80e3a7439d channel name on main menu :3 2022-10-22 14:33:31 +02:00
Ashley
156fe0e276 fix some issues :3 2022-10-20 18:29:06 +02:00
Ashley
1358058c89 auto refresh after 10 secs 2022-10-20 17:29:29 +02:00
Ashley
5118b4aa11 from da web to searches!!! 2022-10-20 17:24:46 +02:00
Ashley
3593b547fa from da web to searches!!! 2022-10-20 17:22:48 +02:00
Ashley
de646f67c8 fix channel name :p 2022-10-20 17:08:59 +02:00
Ashley
963a2b602c new search page !!!! 2022-10-20 16:39:58 +02:00
Ashley
0ac8eb1879 new search page :3 2022-10-20 16:39:00 +02:00
Ashley
abe085bf08 VIDEO PROXY IS BACK WOOO 2022-10-19 21:11:56 +02:00
Ashley
e2732695c7 subscriber counts are back :3 2022-10-19 19:22:37 +02:00
Ashley
5da5082344 fix some issues :3 2022-10-19 16:34:50 +02:00
Ashley
4d153d6b54 new layout :^ (BETA) 2022-10-18 22:46:30 +02:00
Ashley
0a5de4bb44 fix centered issue 2022-10-16 13:01:26 +02:00
Ashley
96de6f43ca qwq 2022-10-16 12:07:26 +02:00
Ashley
f6962c7659 Comment Improvements pog 2022-10-16 12:05:39 +02:00
Ashley
47ffa215bb fix some issues :3 2022-10-16 09:19:12 +02:00
Ashley
435596ba8c qwq 2022-10-14 21:30:04 +02:00
Ashley
9aea8f8370 Mobile Improvements 2022-10-14 20:03:14 +02:00
Ashley
50815f763b new trends :3 2022-10-14 19:46:12 +02:00
Ashley
29c26bb995 new trends! 2022-10-14 19:45:30 +02:00
Ashley
de62a1bc08 new istance url 2022-10-14 18:15:40 +02:00
Ashley
0c2aff9c20 fix some comments issue 2022-10-14 16:18:26 +02:00
Ashley
62f26be024 qwq 2022-10-14 16:16:38 +02:00
Ashley
e1b453127d new formating 2022-10-14 15:54:27 +02:00
Ashley
ac2dcf7515 new stuff :3 2022-10-14 15:53:27 +02:00
Ashley
ee82798c84 Have to use the indevlious proxy for now 2022-10-13 21:44:16 +02:00
Ashley
55f3db380e qwq 2022-10-13 21:43:10 +02:00
Ashley
f0679acd28 Update 'server.js' 2022-10-13 21:20:37 +02:00
Ashley
7006c494e7 remove lighttube proxy 2022-10-13 20:48:58 +02:00
Ashley
9ff773a6f1 fix a description issue :3 2022-10-13 18:22:37 +02:00
Ashley
0fbdd6b000 fix a issue 2022-10-13 17:49:10 +02:00
Ashley
14394c3da2 comments on mobile :3 2022-10-13 16:13:27 +02:00
Ashley
c56683ceda yes 2022-10-13 15:55:01 +02:00
Ashley
6a972ce73d Genre 2022-10-12 22:17:28 +02:00
Ashley
8fbc2c308f hashtags! 2022-10-12 21:54:34 +02:00
Ashley
3a1b1c827c libreredirect 2022-10-12 21:26:00 +02:00
Ashley
6e1304c497 so basically i uhhhhhh 2022-10-12 20:38:29 +02:00
Ashley
52fb2ad099 qwq 2022-10-12 17:17:09 +02:00
Ashley
925823e9f5 comments :3 2022-10-12 17:04:41 +02:00
Ashley
c27e5afbca comments :3 2022-10-12 17:03:43 +02:00
Ashley
6ca17db5cc comments !!!!!! 2022-10-12 17:02:18 +02:00
Ashley
3518939002 Update 'src/pt-api.js' 2022-10-12 16:40:31 +02:00
Ashley
55d28b19d7 faster fetching :3 2022-10-11 16:43:22 +02:00
Ashley
0f2a54bb50 new tabs 2022-10-11 16:41:42 +02:00
Ashley
4227fd72b7 new embed!!!! 2022-10-10 17:54:40 +02:00
Ashley
53cd7c1eb3 new privacy policy :3 2022-10-09 22:38:48 +02:00
Ashley
d6481d0ac4 h 2022-10-09 15:27:41 +02:00
Ashley
f680ebf103 PokeTube Player v09.10.22a 2022-10-09 15:11:28 +02:00
Ashley
1f2eb46184 fix a issue 2022-10-09 14:58:53 +02:00
Ashley
b5bfeab15a fixed the ReferenceError: Cannot access 'channel' before initialization error 2022-10-09 14:38:40 +02:00
Ashley
cf46f15a0c Refactor file 2022-10-09 14:30:08 +02:00
Ashley
5ae14bc68b NEW BACK-END!!! 2022-10-09 12:37:45 +02:00
Ashley
0178f26017 NEW BACK-END!!! 2022-10-09 12:34:39 +02:00
Ashley
fdf1ce8e9a qwq 2022-10-08 16:50:54 +02:00
Ashley
c909c848c3 PokeTube Player v07.10.22a 2022-10-07 16:01:45 +02:00
Ashley
34c623bec9 newwu desc 2022-10-07 15:04:16 +02:00
Ashley
be2a55bc65 h 2022-10-05 17:52:57 +02:00
Ashley
5a62f98b03 Embeds on searches!!! 2022-10-05 17:45:58 +02:00
Ashley
dcb8eab33e harlem shake easter egg!! 2022-10-04 22:09:46 +02:00
Ashley
4e040d9c8a tabs on discover :3 2022-10-03 19:18:14 +02:00
Ashley
b9226fcecd tabs on discover :3 2022-10-03 19:15:54 +02:00
Ashley
f50b783c6d new domains page~ 2022-10-02 22:11:36 +02:00
Ashley
e1e0630bd0 qwq 2022-10-02 22:09:05 +02:00
Ashley
3e4b677be2 instances.hjz 2022-10-02 22:07:20 +02:00
Ashley
f895b23b2c new 404 page!!! 2022-10-02 20:40:19 +02:00
Ashley
069992d3d4 yk what this is tbh 2022-10-01 17:34:11 +02:00
Ashley
9fccf390a4 PokeTube Player v30.09.22a :3 2022-09-30 18:50:18 +02:00
Ashley
bcbef08107 NEW FONTTTTTTTT 2022-09-30 18:42:33 +02:00
Ashley
bbacfe2910 reformat file 2022-09-29 21:15:42 +02:00
Ashley
12de0df326 fix some issues regarding the api :3 2022-09-29 20:59:53 +02:00
Ashley
3dde711bf8 dnoreplace 2022-09-29 19:19:12 +02:00
Ashley
0a2202afdc dnoreplace :3 2022-09-29 19:17:54 +02:00
Ashley
99dfc45782 qwq 2022-09-27 22:00:02 +02:00
Ashley
e18e8d6074 new embed!!!! 2022-09-27 20:04:46 +02:00
Ashley
aa86b3fbaf new embed!!!! 2022-09-27 19:57:07 +02:00
Ashley
670cc8b174 i forgor to proxy dis 2022-09-27 17:16:40 +02:00
Ashley
f940115d3d PROXY EVERYTHING! 2022-09-27 17:01:28 +02:00
Ashley
902e0c8530 PROXY EVERYTHING! 2022-09-27 16:59:18 +02:00
Ashley
5925f2de8b PROXY EVERYTHING! 2022-09-27 16:58:24 +02:00
Ashley
efdfa8a3ad PROXY EVERYTHING! 2022-09-27 16:57:40 +02:00
Ashley
63cf8da561 proxy font awsome 2022-09-27 16:55:35 +02:00
Ashley
72424969b3 PROXY EVERYTHING! 2022-09-27 16:54:06 +02:00
Ashley
6312a11f80 fix a issue 2022-09-26 21:10:46 +02:00
Ashley
68415d6073 PokeTube Player v26.09.22a :3 2022-09-26 17:22:02 +02:00
Ashley
3d60bd3983 fix a issue 2022-09-26 17:09:50 +02:00
Ashley
ad2eacbfae continuation on channels! 2022-09-25 19:25:35 +02:00
Ashley
49ca4992cf FIX MY SKILL ISSUEEE 2022-09-25 19:24:58 +02:00
Ashley
018472e4b4 continuations!!! 2022-09-25 19:16:30 +02:00
Ashley
5712a5be07 continuations!!! 2022-09-25 18:05:23 +02:00
Ashley
5715ecdb96 continuations!!! 2022-09-25 18:04:25 +02:00
Ashley
5cd19daa7e new margin 2022-09-25 15:10:23 +02:00
Ashley
e10af9ee7d qwq 2022-09-25 15:05:00 +02:00
Ashley
2828d784c5 PokeTube Player v25.09.22b :3 2022-09-25 14:58:02 +02:00
Ashley
f0f263c367 PokeTube Player v25.09.22a 2022-09-25 09:18:22 +02:00
Ashley
b01ebd602f inter font for titles :3 2022-09-25 08:22:29 +02:00
Ashley
6e20227e2e change the formatting 2022-09-25 08:18:44 +02:00
Ashley
5266951db8 reformat file 2022-09-24 22:52:20 +02:00
Ashley
c28c0f2f92 qwq 2022-09-24 15:16:28 +02:00
Ashley
b73ab9186e PokeTube Player v24.09.22c :3 2022-09-24 15:14:45 +02:00
Ashley
17468b0397 black background :3 2022-09-24 13:44:58 +02:00
Ashley
ad74c1f940 new colors pog 2022-09-24 13:42:02 +02:00
Ashley
c8c8d25a87 views 2022-09-24 13:20:45 +02:00
Ashley
a0bb3402a5 center align 2022-09-24 12:58:09 +02:00
Ashley
982ea0b216 qwq 2022-09-24 12:51:10 +02:00
Ashley
bbe69891c1 more from the channel :3 2022-09-24 12:37:17 +02:00
Ashley
3e277497b7 text-align:center; 2022-09-24 11:08:56 +02:00
Ashley
c4c1572238 fix some issues 2022-09-24 10:21:13 +02:00
Ashley
52c8d3dd19 make it two trys to be faster 2022-09-23 19:00:57 +02:00
Ashley
5a79091211 fix some issues regarding the api :3 2022-09-23 18:54:05 +02:00
Ashley
6a62772eb2 add subtitles to embed 2022-09-23 17:07:04 +02:00
Ashley
f38796b1ff you get it i think 2022-09-23 15:46:41 +02:00
Ashley
f57c2b9471 fix some issues on the channel page 2022-09-23 15:30:50 +02:00
Ashley
14ac36baba new navbar on mobile for channels :3 2022-09-23 15:04:49 +02:00
Ashley
62f668e4ad new channel page for mobile :3 2022-09-23 13:27:53 +02:00
Ashley
2e0e929e91 new channel page for mobile :3 2022-09-23 13:25:02 +02:00
Ashley
8c1575480a you get it i think 2022-09-22 18:07:11 +02:00
Ashley
6642dc157b formatted likes & dislikes 2022-09-22 16:28:28 +02:00
Ashley
4fa57c1146 new number formatter 2022-09-22 16:25:31 +02:00
Ashley
c543ae840a PokeTube Player v22.09.22a 2022-09-22 13:48:07 +02:00
Ashley
4353d1c7d9 convert function 2022-09-22 13:46:56 +02:00
Ashley
af1efb12d5 add a iframe embed :3 2022-09-22 12:06:54 +02:00
Ashley
0bae1b7920 new embeds :3 2022-09-22 12:05:32 +02:00
Ashley
a23c78397b qwq 2022-09-21 15:16:40 +02:00
Ashley
b19755101d qwq 2022-09-21 14:41:14 +02:00
Ashley
0758c10fa5 new stuff 2022-09-21 14:34:25 +02:00
Ashley
61ca5e12ef fix some issues on the channel page 2022-09-21 14:33:49 +02:00
Ashley
e0bcf41f15 add no desc warning 2022-09-21 14:21:36 +02:00
Ashley
942b3b2ef7 h 2022-09-21 11:55:16 +02:00
Ashley
395f2769b4 fix a description issue 2022-09-21 11:53:21 +02:00
Ashley
d3b700c710 fix a description issue 2022-09-21 11:52:41 +02:00
Ashley
25da427803 new about section :3 2022-09-21 10:50:55 +02:00
Ashley
6dfe6f2cc9 new about section :3 2022-09-21 10:50:15 +02:00
Ashley
4036e409fa MY BAD FVHRbYHTG 2022-09-21 09:55:04 +02:00
Ashley
e6dddefb4b proxy images 2022-09-21 09:49:36 +02:00
Ashley
ca61c444f1 fix a issue 2022-09-21 09:43:13 +02:00
Ashley
448c697388 new channel page :3 2022-09-21 09:36:33 +02:00
Ashley
9d75ff1eab Reformat main file, add embeds :3 2022-09-21 08:36:21 +02:00
Ashley
f08e87d3e9 fix a issue 2022-09-20 18:53:29 +02:00
Ashley
a5b027b14b PokeTube Player v20.09.22a 2022-09-20 18:41:56 +02:00
Ashley
854527ef41 qwq 2022-09-20 16:08:03 +02:00
Ashley
7360610439 fix lyrics issue 2022-09-18 18:58:22 +02:00
Ashley
d65d90d432 fix some issues regarding the api :3 2022-09-18 17:20:19 +02:00
Ashley
12f3806764 FIX MY SKILL ISSUEEE 2022-09-18 15:57:39 +02:00
Ashley
823dfd9924 new padding 2022-09-18 15:54:51 +02:00
Ashley
8ecd3d3a60 FIX MY SKILL ISSUEEE 2022-09-17 20:56:14 +02:00
Ashley
661aac0c15 add from param 2022-09-17 20:49:20 +02:00
Ashley
eb22c8d52c search option! 2022-09-17 20:46:58 +02:00
Ashley
d3a54be450 search option! 2022-09-17 20:45:47 +02:00
Ashley
e6491e2a9e Add 'opensearch.xml' 2022-09-17 20:43:49 +02:00
Ashley
482f69f3f3 from the web / smort desc 2022-09-17 18:23:32 +02:00
Ashley
9f4e1321b5 smart desc :3 2022-09-17 18:22:47 +02:00
Ashley
6890b9fc42 smart desc! 2022-09-17 18:22:01 +02:00
Ashley
b2bf41f406 qwq 2022-09-17 17:51:22 +02:00
Ashley
b71e5660f6 PokeTube Player v17.09.22a :3 2022-09-17 15:18:50 +02:00
Ashley
bbb68d811a remove google fonts 2022-09-17 13:51:12 +02:00
Ashley
45e88ee44f Update 'html/main.ejs' 2022-09-15 18:01:07 +02:00
Ashley
7deba592ab i forgor 2022-09-14 06:05:34 +02:00
Ashley
7505696afd qwq 2022-09-13 21:06:26 +02:00
Ashley
3e911d41a5 fix div issues 2022-09-13 20:12:07 +02:00
Ashley
195871618f Update 'html/main.ejs' 2022-09-13 19:58:22 +02:00
Ashley
726972507e Add amoled mode in mobile~ 2022-09-13 19:18:13 +02:00
Ashley
12229238a4 qwq 2022-09-13 16:30:55 +02:00
Ashley
74bab2615b qwq 2022-09-12 18:04:15 +02:00
Ashley
bead9005fb new policy :3 2022-09-12 17:53:17 +02:00
Ashley
0311d8f489 new search netrics 2022-09-12 17:35:54 +02:00
Ashley
9f9a5035de fix some issues 2022-09-11 21:52:15 +02:00
Ashley
0a4a0d1982 yes 2022-09-10 17:14:11 +02:00
Ashley
baed71669a PokeTube Player v09.09.22c :3 2022-09-09 20:16:18 +02:00
Ashley
5a27c255e4 PokeTube Player v09.09.22b :3 2022-09-09 17:07:26 +02:00
Ashley
2babed2d40 PokeTube Player v09.09.22a 2022-09-09 13:59:39 +02:00
Ashley
d7ead9b254 Add 'css/watch-navbar.css' 2022-09-09 12:56:26 +02:00
Ashley
2cb2fdb80c watch util css 2022-09-09 12:55:45 +02:00
Ashley
28b83178b2 add margin in title 2022-09-04 23:20:25 +02:00
Ashley
72a9cad77a improvments 2022-09-04 23:08:02 +02:00
Ashley
2095f11b05 comments on mobile :3 2022-09-04 23:01:42 +02:00
Ashley
d9657e5034 new tga 2022-09-04 22:22:09 +02:00
Ashley
f759e0bed3 yoink 2022-09-04 21:41:00 +02:00
Ashley
2525c9f165 badges in mobile :3 2022-09-04 11:32:14 +02:00
Ashley
b1ea23ebd9 comments :3 2022-09-04 11:00:51 +02:00
Ashley
651f1326ea comments :3 2022-09-04 10:50:46 +02:00
Ashley
4a9d513b88 qwq 2022-09-04 09:31:02 +02:00
Ashley
0edc0c5c5d FIX MY SKILL ISSUEEE 2022-09-04 09:29:52 +02:00
Ashley
339fc9b0d3 Verified icons :3 2022-09-04 09:01:04 +02:00
Ashley
579bc290c6 owo 2022-09-04 08:56:59 +02:00
Ashley
c771a71847 yes 2022-09-02 17:31:04 +02:00
Ashley
3fe3b4ce91 FIX MY SKILL ISSUEEE 2022-09-02 14:12:37 +02:00
Ashley
82d579a2c3 badges (beta) 2022-09-02 14:11:54 +02:00
Ashley
ab0e70c6ea remove google fonts 2022-09-01 15:30:33 +02:00
Ashley
5bfca3c698 remove google fonts 2022-09-01 15:29:37 +02:00
Ashley
bbb07b65ef no more google fonts 2022-09-01 14:56:31 +02:00
Ashley
93316973f6 no more google fonts 2022-09-01 14:54:15 +02:00
Ashley
85ec2bc651 no more google fonts 2022-09-01 14:53:27 +02:00
Ashley
7fda6c1dd8 center align 2022-09-01 13:45:38 +02:00
Ashley
cef1a26760 fix some issues 2022-09-01 13:45:13 +02:00
Ashley
bbf0dc0234 i forgor 💀 2022-09-01 13:36:08 +02:00
Ashley
ae8eea7eea i forgor 2022-09-01 13:34:49 +02:00
Ashley
bf6a789aff new header pog 2022-09-01 13:31:25 +02:00
Ashley
09cff712ec new header :3 2022-09-01 13:30:39 +02:00
Ashley
a1c250d6e4 new header 2022-09-01 13:28:01 +02:00
Ashley
41b8db82bf new buttons :3 2022-08-31 14:37:17 +02:00
Ashley
867494e6a1 center align 2022-08-31 11:29:26 +02:00
Ashley
7e1382fca6 flex div 2022-08-31 11:04:47 +02:00
Ashley
7c01c5e00f new buttons :3 2022-08-31 10:09:26 +02:00
Ashley
6715611fb3 fix some typos 2022-08-29 19:24:28 +02:00
Ashley
6349f8d8df new landing !!!!!!111 2022-08-29 19:12:40 +02:00
Ashley
e928d70d3c new url :3 2022-08-29 18:59:29 +02:00
Ashley
be587ddb08 FIX MY SKILL ISSUEEE 2022-08-29 18:58:18 +02:00
Ashley
26889c2ebd new landing !!!!!!111 2022-08-29 18:57:30 +02:00
Ashley
392ee595b1 h 2022-08-29 16:13:58 +02:00
Ashley
74099a54d9 PokeTube Player v08.29.22 :3 2022-08-29 14:35:29 +02:00
Ashley
22ac2fad9b FIX MY SKILL ISSUEEE 2022-08-27 16:07:28 +02:00
Ashley
c0fece4486 Add Share on Mobile 2022-08-27 15:58:14 +02:00
Ashley
172c693cca add support for audio music 2022-08-27 15:39:21 +02:00
Ashley
aab6f525e2 reformat file 2022-08-27 15:23:46 +02:00
Ashley
264b1349a4 remove /old 2022-08-27 15:17:43 +02:00
Ashley
b3bc8aa73e PokeTube Player v08.27.22 :3 2022-08-27 15:16:29 +02:00
Ashley
1f95de7178 PokeTube Player v08.26.23 :3
Quailty settings on mobile !
2022-08-26 21:03:29 +02:00
Ashley
6d5a4a6101 new readme pog 2022-08-25 13:40:24 +02:00
Ashley
02808c1a6c new readme 2022-08-25 13:34:00 +02:00
Ashley
f7228fe545 if no song dont go to / music lmafo 2022-08-25 12:12:57 +02:00
Ashley
c7577dc8f7 AMAZING 2022-08-25 11:16:25 +02:00
Ashley
da20dd1001 FIX MY SKILL ISSUEEE 2022-08-24 15:27:02 +02:00
Ashley
bb888adcd7 error msg good 2022-08-24 15:22:16 +02:00
Ashley
11079b5992 yes 2022-08-24 13:24:31 +02:00
Ashley
56dc61dd16 poketube Player v08.23.22 2022-08-24 12:06:18 +02:00
Ashley
6332fcab1d Update 'html/poketube.ejs' 2022-08-24 11:38:44 +02:00
Ashley
57696caed0 depracate old u, 2022-08-23 23:09:51 +02:00
Ashley
a6e6abc78e yes 2022-08-23 16:22:59 +02:00
Ashley
9eb2856b12 Update 'server.js' 2022-08-23 14:04:34 +02:00
Ashley
1303c0a6db Update 'html/poketube.ejs' 2022-08-23 14:01:17 +02:00
Ashley
70fd65d237 qwq 2022-08-22 23:24:22 +02:00
Ashley
880e494ebc yaasss 2022-08-22 16:34:39 +02:00
Ashley
cd0dc83a79 Update 'html/license.ejs' 2022-08-22 12:49:55 +02:00
Ashley
98090dd873 New href colors 2022-08-22 12:25:44 +02:00
Ashley
e02e0fe3fb fix some typos 2022-08-22 11:49:21 +02:00
Ashley
b1c3e8d310 new policy :3 2022-08-22 11:47:36 +02:00
Ashley
58eed4028e ye 2022-08-22 11:02:34 +02:00
Ashley
675193da07 Update 'css/pv.main.css' 2022-08-22 11:01:20 +02:00
Ashley
a0e8317c7a Update 'css/pv.main.css' 2022-08-22 10:39:14 +02:00
Ashley
403836ac18 reformat file 2022-08-22 10:36:46 +02:00
Ashley
c365473ef9 reformat file 2022-08-22 10:28:01 +02:00
Ashley
125185c630 new colors pog 2022-08-22 08:19:47 +02:00
Ashley
e3041179eb server info :3 2022-08-22 06:31:22 +02:00
Ashley
eb42861a98 server info :3 2022-08-21 15:29:40 +02:00
Ashley
d21c877e98 ip info 2022-08-21 15:28:35 +02:00
Ashley
68ff7be74a remove ytdl-core 2022-08-21 15:27:35 +02:00
Ashley
009319bc30 add procces information 3 2022-08-21 11:43:02 +02:00
Ashley
069b98715f add procces information :^ 2022-08-21 11:40:39 +02:00
Ashley
cf9d34efdc Update 'html/main.ejs' 2022-08-20 16:19:46 +02:00
Ashley
7dc2674bd5 auto overflow on lyrics 2022-08-20 03:50:58 +02:00
Ashley
7ddbca540e 2vh margin :3 2022-08-20 01:27:22 +02:00
Ashley
812c4a1df2 Lyrics qwq 2022-08-19 23:33:55 +02:00
Ashley
0dbc93ac0f lyricsss 2022-08-19 23:32:38 +02:00
Ashley
9802cad2b0 update thignys 2022-08-19 18:24:44 +02:00
Ashley
e3a19e57d7 cool stuff 2022-08-19 11:06:24 +02:00
Ashley
a396fd8ab3 Update 'html/poketube-music.ejs' 2022-08-19 10:34:52 +02:00
Ashley
1e2e87411b add apple music logo 2022-08-19 10:34:17 +02:00
Ashley
bb5e0b1dc5 yus 2022-08-19 09:22:25 +02:00
Ashley
b8ab8b47b2 Update 'server.js' 2022-08-19 09:21:44 +02:00
Ashley
6ada0bb34c h 2022-08-19 00:21:09 +02:00
Ashley
75a9853dca owo 2022-08-19 00:17:53 +02:00
Ashley
88c8706c97 yes 2022-08-19 00:17:03 +02:00
Ashley
c12ab5f71b Update 'html/poketube-music.ejs' 2022-08-18 23:06:56 +02:00
Ashley
2f686f7f59 Update 'server.js' 2022-08-18 23:06:09 +02:00
Ashley
4798d8fa04 Update 'package.json' 2022-08-18 23:05:08 +02:00
Ashley
7fa521d70c Delete 'p/lyrics.js' 2022-08-18 13:48:10 +02:00
Ashley
333ac4eb60 new logo :3 2022-08-18 13:45:57 +02:00
Ashley
29b7a53372 Update 'src/lyrics.js' 2022-08-18 13:43:42 +02:00
Ashley
6e8454109a g 2022-08-18 13:41:43 +02:00
Ashley
65a40ed308 Add 'css/music.sv' 2022-08-18 13:41:21 +02:00
Ashley
5093b2f543 new policy owo 2022-08-18 04:32:35 +02:00
Ashley
856fbdd6b6 Update 'html/poketube-music.ejs' 2022-08-18 03:49:53 +02:00
Ashley
8044ee9aa8 (for now) remove video proxy 2022-08-18 03:49:16 +02:00
Ashley
abb32e6d79 Update 'html/poketube.ejs' 2022-08-17 17:24:29 +02:00
Ashley
f12af1a26e Update 'html/poketube.ejs' 2022-08-17 01:10:16 +02:00
Ashley
34ffe0151c AMAZING 2022-08-17 00:27:10 +02:00
Ashley
3944856c87 Update 'p/server.js' 2022-08-17 00:20:26 +02:00
Ashley
5d615bf057 yes 2022-08-17 00:19:00 +02:00
Ashley
40a5c5f466 owo 2022-08-16 18:35:07 +02:00
Ashley
1bcfa02a95 poketube music n stuff 2022-08-16 18:33:28 +02:00
Ashley
2b127e85f2 music owo 2022-08-16 18:32:45 +02:00
Ashley
27c96be635 the duck 2022-08-15 19:41:52 +02:00
Ashley
13e30ec482 yaasss 2022-08-15 12:23:27 +02:00
Ashley
f6abeaa97b Update 'p/server.js' 2022-08-15 11:58:08 +02:00
Ashley
b32bf35896 Add 'p/lyrics.js' 2022-08-15 11:57:24 +02:00
Ashley
7fd8b8ef4c owo 2022-08-15 11:56:26 +02:00
Ashley
88f955f2da Update 'src/lyrics.js' 2022-08-15 11:55:41 +02:00
Ashley
ac48876c03 pwosy owo 2022-08-15 11:09:02 +02:00
Ashley
2c2aa9ba3d reformat file 2022-08-15 10:54:24 +02:00
Ashley
83ccd7edc7 Proxy OWO~ 2022-08-15 10:53:02 +02:00
Ashley
a0f6fd92fa Add 'p/readme' 2022-08-15 10:28:28 +02:00
Ashley
16343b0ed1 Proxy 2022-08-15 10:27:40 +02:00
Ashley
9161f511d7 pro 2022-08-14 23:14:37 +02:00
Ashley
a7c8024ceb new issue url 2022-08-14 11:08:19 +02:00
Ashley
a4eacd42ca yes 2022-08-13 21:36:41 +02:00
Ashley
f5b4b6263d new git url :3 2022-08-13 21:20:01 +02:00
Ashley
c98bd5ecf0 Update 'README.md' 2022-08-13 21:12:45 +02:00
Ashley
bc2a23b0c3 yes 2022-08-12 21:44:27 +03:00
Ashley
7805c9fbd0 padding 2022-08-12 21:04:52 +03:00
Ashley
b63a71d236 YESSSSSSS 2022-08-12 21:02:07 +03:00
Ashley
e573020440 Update 404.ejs 2022-08-12 18:05:52 +03:00
Ashley
00dbaae3b8 new 404 yes yes 2022-08-12 18:04:34 +03:00
Ashley
742af05467 yee 2022-08-12 16:46:36 +03:00
Ashley
7dfefad0bc mobile support 2022-08-12 15:04:52 +03:00
Ashley
f84e66c3e8 isMobile 2022-08-12 15:04:09 +03:00
Ashley
4304cab592 yes 2022-08-12 14:55:35 +03:00
Ashley
a0fc06a0c9 h 2022-08-12 03:46:24 +03:00
Ashley
35b21cffd5 you know 2022-08-12 03:21:53 +03:00
Ashley
5e725f952b ye 2022-08-12 03:20:36 +03:00
Ashley
5031d08de6 mmmmmmmmmmmmmmmmmmm 2022-08-12 03:19:47 +03:00
Ashley
d774b43ec5 gap 2022-08-12 03:13:16 +03:00
Ashley
ae2d9dfda7 Update download.ejs 2022-08-12 03:06:08 +03:00
Ashley
f5228bece9 I DONT LIKE
THE
2022-08-10 22:49:08 +03:00
v4ltages
33ed6e3886 fix quality buttons 2022-08-10 22:34:28 +03:00
Ashley
f2f333e996 Merge pull request #11 from v4ltages/main
change search bar
2022-08-10 22:14:46 +03:00
v4ltages
c523c86124 change search bar 2022-08-10 00:42:18 +03:00
Ashley
2b721f070f border radiussssss 2022-08-09 15:48:34 +03:00
Ashley
f071fef155 new logo qwq 2022-08-09 15:35:54 +03:00
Ashley
607036d58f Coding Standards yesyes 2022-08-09 12:39:18 +03:00
Ashley
4b2092a909 this looks better tbh 2022-08-09 09:11:54 +03:00
Ashley
24447c0d46 qwq 2022-08-09 08:44:45 +03:00
Ashley
3ef3663b6a Merge pull request #10 from v4ltages/main
deminified css for easier development and fixing
2022-08-09 08:28:24 +03:00
v4ltages
07988a75d0 readd engine requirement 2022-08-09 03:05:01 +03:00
v4ltages
67ce46f328 css adjustments 2022-08-09 03:00:07 +03:00
Ashley
9710cd6f07 yes 2022-08-09 02:39:07 +03:00
Ashley
e40fc5a297 h
add space around to videos list
2022-08-09 01:29:01 +03:00
v4ltages
e2460503c8 add space-around 2022-08-09 01:11:29 +03:00
v4ltages
c77feba6e1 add space-around 2022-08-09 01:10:09 +03:00
Ashley
5d8947e880 hiiiiiiiiiiii 2022-08-08 21:36:31 +03:00
Ashley
6de78d95f6 yes 2022-08-08 21:35:48 +03:00
Ashley
4c79de16fa good stuff 2022-08-08 21:35:08 +03:00
Ashley
4751fe0874 resolution~ 2022-08-08 19:49:57 +03:00
Ashley
ea94850d38 resolution les goo 2022-08-08 19:48:56 +03:00
Ashley
6599c4aba5 cool stuff 2022-08-08 18:11:23 +03:00
Ashley
a8cc7ec642 qwq 2022-08-08 18:08:27 +03:00
Ashley
5760006c9d h 2022-08-08 16:05:40 +03:00
Ashley
c29139678d im tired at this moment 2022-08-08 16:03:31 +03:00
Ashley
8b71402b56 new logo qwqqqqqqqqqqqqqqqq 2022-08-08 16:01:38 +03:00
Ashley
b4d6b476c2 you get the rest 2022-08-08 15:58:18 +03:00
Ashley
25c6749ab6 new logo width qwq 2022-08-08 15:55:47 +03:00
Ashley
623b8b6823 new logo width 2022-08-08 15:54:45 +03:00
Ashley
f6ff344804 Update README.md 2022-08-08 15:51:13 +03:00
Ashley
db03788603 qwq 2022-08-08 15:49:47 +03:00
Ashley
ec9b91af8a new logo qwq 2022-08-08 15:48:56 +03:00
Ashley
5ad5f3489f new logo qwq 2022-08-08 13:59:07 +03:00
Ashley
0decf52be0 new mobile stuff 2022-08-07 19:31:59 +03:00
Ashley
a84b7c4fc1 more formaaaaaaatsss 2022-08-07 18:32:47 +03:00
Ashley
612a0de50d new download stuff owo 2022-08-07 18:31:07 +03:00
Ashley
a47319c625 you know the rest 2022-08-07 18:02:15 +03:00
Ashley
a25d4ea2ae no more skill issue 2022-08-07 18:00:36 +03:00
Ashley
46573ece85 hi 2022-08-07 16:33:07 +03:00
Ashley
b07f92a677 encryption url 2022-08-07 16:30:00 +03:00
Ashley
436975b202 yes 2022-08-07 16:17:47 +03:00
Ashley
0f2621c41c download owo 2022-08-07 16:16:59 +03:00
Ashley
810d1a5dde hewwo 2022-08-07 15:24:25 +03:00
Ashley
a34668da62 Encryption info 2022-08-06 22:49:20 +03:00
Ashley
e11eb69edc encryption stuff 2022-08-06 22:22:46 +03:00
Ashley
ff1aa34517 encryption json 2022-08-06 22:19:04 +03:00
Ashley
19757f793c remove /mobile qwq 2022-08-06 20:09:20 +03:00
Ashley
0adb077c54 mobile owo 2022-08-06 20:07:06 +03:00
Ashley
260cee49c3 Delete poketube-mobile.ejs 2022-08-06 20:06:07 +03:00
Ashley
510e370f23 me when the 2022-08-06 17:11:23 +03:00
Ashley
a719a9bdf8 new buttions :3 2022-08-06 16:32:24 +03:00
Ashley
44a5983c2b owo 2022-08-06 16:30:45 +03:00
Ashley
e7f4ae9bdb no more skill issue 2022-08-06 15:18:29 +03:00
Ashley
b8fce7d3e1 squid games
Readme update
2022-08-06 15:14:03 +03:00
Thanos Apollo
44e71a3d70 Update README.md 2022-08-06 15:12:41 +03:00
Thanos Apollo
3111d98f04 fix typo 2022-08-06 14:58:28 +03:00
ThanosApollo
e6eb4da3eb Update README, add usage,ToC and fix hyperlinks 2022-08-06 14:54:04 +03:00
Ashley
76c050b1f5 Update README.md 2022-08-05 22:35:31 +03:00
Ashley
63dab6176e owo 2022-08-05 22:33:38 +03:00
Ashley
d2874c2e5f Create README 2022-08-05 22:27:54 +03:00
Ashley
b4176f4cbb WHAT THE HELL IS ENCRYPTION ?! 2022-08-05 22:26:09 +03:00
Ashley
b7ec22aa89 yusssssssssssss 2022-08-05 22:25:25 +03:00
Ashley
f0e6d7b7cf yus 2022-08-05 16:43:40 +03:00
Ashley
ab85606680 hewwo :^ 2022-08-04 19:40:40 +03:00
Ashley
1ec1e95beb remove the p
yeet
2022-08-04 12:23:12 +03:00
Ashley
be445b6467 Encryption Information! :3 2022-08-04 12:12:24 +03:00
Ashley
c0e7181749 ECDSA text :3 2022-08-04 00:54:40 +03:00
Ashley
330ef078a6 sha384 yes yes 2022-08-04 00:53:26 +03:00
Ashley
801c4aa12f sha512 pog 2022-08-04 00:52:31 +03:00
Ashley
44e50fcd0f SHA-384 owo :3 2022-08-04 00:51:21 +03:00
Ashley
0b0b2a52c1 yus 2022-08-03 23:22:25 +03:00
Ashley
534ff24f5a 0.3 pc --> 0.5pc it looks better tbh 2022-08-02 23:03:02 +03:00
Ashley
bacbd6ddff new borders pog 2022-08-02 22:58:30 +03:00
Ashley
a58fd28fb2 the 2022-08-02 20:03:42 +03:00
Ashley
5bed636b7c owo 2022-08-02 01:46:17 +03:00
Ashley
ba5ea8cfbe yus 2022-08-01 20:42:00 +03:00
Ashley
e6f8a6b357 sdk license owo 2022-08-01 06:26:33 +03:00
Ashley
72fed39369 new navbar color :^ 2022-08-01 04:38:04 +03:00
Ashley
9bd7a2f2ce wooooo new navbar
how fun innit
2022-07-31 16:51:23 +03:00
Ashley
a6cf288c56 owo new page~ 2022-07-31 16:49:24 +03:00
Ashley
e647f6628c Update poketube.ejs 2022-07-31 12:41:20 +03:00
Ashley
18db8eac93 add spaces owo 2022-07-30 01:45:28 +03:00
Ashley
b844742998 Update main.ejs 2022-07-29 18:54:56 +03:00
Ashley
5c3b720c98 new policy owo 2022-07-29 16:01:46 +03:00
Ashley
69533461ed licenses owo 2022-07-29 15:27:26 +03:00
Ashley
1f7192363f Update server.js 2022-07-29 15:26:00 +03:00
Ashley
7bd7eed72a improve stuff :3 2022-07-29 14:21:05 +03:00
Ashley
ca6d3fa0c4 Update main.ejs 2022-07-29 00:49:44 +03:00
Ashley
e07f2e1ded add spaces :D 2022-07-28 01:17:05 +03:00
Ashley
d9d4aa68e5 Update main.ejs 2022-07-27 18:28:53 +03:00
Ashley
0539799332 Update poketube-mobile.ejs 2022-07-27 18:28:32 +03:00
Ashley
d82a05153d Update poketube-mobile.ejs 2022-07-27 00:24:23 +03:00
Ashley
65bf0f4f5f Update poketube-mobile.ejs 2022-07-26 15:26:07 +03:00
Ashley
aee7df7204 Update poketube-mobile.ejs 2022-07-26 13:20:52 +03:00
Ashley
a46cc46a55 Update poketube-mobile.ejs 2022-07-26 09:47:25 +03:00
Ashley
1a32a385c3 Update server.js 2022-07-26 09:46:18 +03:00
Ashley
9e64ed6be5 Update package.json 2022-07-26 09:45:02 +03:00
Ashley
ada1c4391b Rename piwik.js to piwik.min.js 2022-07-26 00:59:39 +03:00
Ashley
ce7cf427ae Create poketube-mobile.ejs 2022-07-26 00:52:39 +03:00
Ashley
7dc3bc6e46 Update server.js 2022-07-26 00:51:11 +03:00
Ashley
a243ff9f74 big rewrite 2022-07-25 14:45:34 +03:00
Ashley
3a2705b554 Update poketube.ejs 2022-07-24 23:07:27 +03:00
Ashley
8eb8816c3b Delete README.md 2022-07-24 18:08:52 +03:00
Ashley
3ade00786b Add files via upload 2022-07-24 18:08:29 +03:00
Ashley
1f76099d30 Create README.md 2022-07-24 18:05:48 +03:00
Ashley
2560cf5030 Delete matomo-4.x-dev.zip 2022-07-24 18:05:12 +03:00
Ashley
07b1379046 Rename t/piwik/matomo.php to t/piwik/files/matomo.php 2022-07-24 18:04:37 +03:00
Ashley
adbeb4b055 Rename t/piwik/piwik.js to t/piwik/files/piwik.js 2022-07-24 18:03:38 +03:00
Ashley
ccaf9a8db9 Rename t/piwik/piwik.php to t/piwik/files/piwik.php 2022-07-24 18:03:02 +03:00
Ashley
469f33e821 Create matomo.php 2022-07-24 18:02:37 +03:00
Ashley
da145babef Update README.md 2022-07-24 18:01:41 +03:00
Ashley
53f8a8cec4 Create piwik.php 2022-07-24 18:00:42 +03:00
Ashley
3439aed7fc Update main.ejs 2022-07-24 17:48:47 +03:00
Ashley
6fe7d0cc11 yes 2022-07-23 17:50:02 +03:00
Ashley
57cea913d0 Create README.md 2022-07-23 12:50:40 +03:00
Ashley
06ee266de9 Create COPYING 2022-07-23 12:47:12 +03:00
Ashley
f8e99640f2 Update priv.ejs 2022-07-22 16:15:56 +03:00
Ashley
edc3428a6c new stuff 2022-07-22 16:00:07 +03:00
Ashley
1656e469ff Update README.md 2022-07-22 00:03:05 +03:00
Ashley
77d6282fb9 Update server.js 2022-07-22 00:02:38 +03:00
Ashley
a820328c29 Update poketube.ejs 2022-07-22 00:02:08 +03:00
Ashley
864d6128a0 Update server.js 2022-07-20 11:58:56 +03:00
Ashley
1a13de138c Update README.md 2022-07-19 22:59:19 +03:00
Ashley
94708f5a72 Update main.ejs 2022-07-19 17:24:54 +03:00
Ashley
5238ffad28 Update server.js 2022-07-18 15:23:18 +03:00
Ashley
af83ff3718 Update main.ejs 2022-07-18 15:22:26 +03:00
Ashley
f4e3e6ba30 Create landing.ejs 2022-07-18 15:21:55 +03:00
Ashley
e403001be5 Update main.ejs 2022-07-18 00:54:18 +03:00
Ashley
ba898b12cd squid games
Grammatical-Fix
2022-07-17 18:21:12 +03:00
NullUsxr
9d9db83a54 Grammatical Fixes 2022-07-17 02:34:10 -06:00
NullUsxr
02a48c0aff Grammatical Fixes 2022-07-17 02:28:20 -06:00
Ashley
834d27245f monke 2022-07-17 11:23:58 +03:00
Ashley
560f866199 add style color
since in chromium,the desc of the video is gray
2022-07-17 11:06:21 +03:00
Ashley
bf491e0a7a Update main.ejs 2022-07-16 20:43:23 +03:00
Ashley
96f0b123cc Update poketube.ejs 2022-07-16 13:05:11 +03:00
Ashley
711dc09c09 Update server.js 2022-07-16 13:03:46 +03:00
Ashley
4187f9b1e6 Update server.js 2022-07-15 16:41:26 +03:00
Ashley
d7bda929a9 Explore Videos 2022-07-15 16:30:02 +03:00
Ashley
acaaae8759 New feed 2022-07-15 16:29:02 +03:00
Ashley
2f33ec80f6 Update main.ejs 2022-07-15 14:26:19 +03:00
Ashley
88167a173a trends owo 2022-07-15 14:24:25 +03:00
Ashley
2bd1b09d23 Create piwik.js 2022-07-14 23:39:56 +03:00
Ashley
cf90b273aa Delete README.md 2022-07-14 23:34:54 +03:00
Ashley
932e0b7fb8 Add files via upload 2022-07-14 23:34:26 +03:00
Ashley
a798aee80c Create README.md 2022-07-14 23:30:22 +03:00
Ashley
bae5200ca0 Update README.md 2022-07-14 13:23:04 +03:00
Ashley
5d3d11b982 Update poketube.ejs 2022-07-13 15:35:55 +03:00
Ashley
148a6142bf Update server.js 2022-07-13 15:35:18 +03:00
Ashley
d695f6a9af squid games 2022-07-13 14:20:20 +03:00
Ashley
534f9ec69a new ui n stuffü 2022-07-13 13:49:51 +03:00
Ashley
b8131740eb yes 2022-07-13 13:48:30 +03:00
Ashley
772a76aa27 add source code url 2022-07-12 22:22:39 +03:00
Ashley
60a3a51ab5 Create style.css 2022-07-12 22:02:38 +03:00
Ashley
08740f1d8f new webpage! 2022-07-12 22:01:58 +03:00
Ashley
1281595549 yes 2022-07-12 21:30:40 +03:00
Ashley
01e072085e download gui! 2022-07-12 20:12:54 +03:00
Ashley
72f60c98e3 new download url! 2022-07-12 20:11:23 +03:00
Ashley
13d5e7afd1 add download 2022-07-12 20:06:02 +03:00
Ashley
95d2567b24 add search 2022-07-12 12:31:01 +03:00
Ashley
060ca8aa8c add "Introduction" 2022-07-12 00:39:09 +03:00
Ashley
14cbc59b37 a better place to it 2022-07-11 20:17:35 +03:00
Ashley
5c84331a52 Add stand with ukraine 2022-07-11 20:16:54 +03:00
Ashley
38eaaab847 yes 2022-07-11 19:16:19 +03:00
Ashley
25bd0b9afa Stats --> Metrics
haha yes
2022-07-11 19:14:14 +03:00
Ashley
06b60f567c Make the policy more clear 2022-07-11 19:10:25 +03:00
Ashley
5956da7d01 remove pt music 2022-07-11 12:48:15 +03:00
Ashley
7af427b1f6 Create poketube-music.ejs 2022-07-11 00:43:05 +03:00
Ashley
82ed9b4bd5 add poketube music 2022-07-11 00:42:25 +03:00
Ashley
575618c8a7 add music (very beta) 2022-07-11 00:41:52 +03:00
Ashley
3e4787921f Create no-logomark.svg 2022-07-10 23:52:45 +03:00
Ashley
94c6125d8b add a blank space 2022-07-10 23:52:00 +03:00
Ashley
7e9266dd59 Update README.md 2022-07-10 23:46:51 +03:00
Ashley
6ba510db8c Update README.md 2022-07-10 23:36:56 +03:00
Ashley
efcec3ec70 Update package.json 2022-07-10 22:32:24 +03:00
Ashley
b5131f45a8 hm 2022-07-10 22:29:54 +03:00
Ashley
6b3502bcae Update README.md 2022-07-09 10:22:20 +03:00
Ashley
bf2233397d Update rep 2022-07-09 10:15:44 +03:00
Ashley
25c3511d19 new piwik lol 2022-07-09 10:06:53 +03:00
Ashley
52d9b0df7e The real reason you can't invade Australia is because it doesn't exist. Australia is not real. It’s a hoax, made for us to believe that Britain moved over their criminals to someplace. In reality, all these criminals were loaded off the ships into the waters, drowning before they could see land ever again. It’s a coverup for one of the greatest mass murders in history, made by one of the most prominent empires. Australia does not exist. All things you call “proof” are actually well fabricated lies and documents made by the leading governments of the world. Your Australian friends? They’re all actors and computer generated personas, part of the plot to trick the world. If you think you’ve ever been to Australia, you’re terribly wrong. The plane pilots are all in on this, and have in all actuality only flown you to islands close nearby – or in some cases, parts of South America, where they have cleared space and hired actors to act out as real Australians. Australia is one of the biggest hoaxes ever created, and you have all been tricked. Join the movement today, and make it known that they have been deceived. Make it known, that this has all just been a cover-up. The things these “Australian” says to be doing, all these swear words and actions based on alcoholism, MDMA and bad decisions, are all ways to distract you from the ugly truth that is one of the greatest genocides in history. 162,000 people was said to have been transported to this imaginary land during a mere 80 years, and they are all long dead by now. They never reached that promised land. 2022-07-07 20:12:03 +03:00
Ashley
905118daf8 Update 404.ejs 2022-07-07 19:00:53 +03:00
Ashley
85efefcf13 yo 2022-07-07 18:16:31 +03:00
Ashley
58e570e124 italic font
bottom text
2022-07-07 18:10:46 +03:00
Ashley
13be82065a new theme pog 2022-07-07 16:57:43 +03:00
Ashley
7255b78fc2 i cant find an funny commit title so nya~ 2022-07-07 14:39:04 +03:00
Ashley
e5725a36d9 newww fooont 2022-07-07 14:37:10 +03:00
Ashley
4696f48505 nya 2022-07-07 14:35:37 +03:00
Ashley
7c0c058765 new font qwq 2022-07-07 14:34:44 +03:00
Ashley
f51eef6c82 new font :3 2022-07-07 14:34:13 +03:00
Ashley
0f227ac90c typo moment 2022-07-06 18:33:59 +03:00
Ashley
04c91fc920 add "your freedom" string lol 2022-07-06 13:12:46 +03:00
Ashley
60873cfa46 Add Liability 2022-07-06 10:40:45 +03:00
Ashley
9cec8620fd Add Other Free YouTube Front-Ends & fix some typos too lol 2022-07-06 10:28:50 +03:00
Ashley
0ffc2d8edd Update priv.ejs 2022-07-05 14:13:39 +03:00
Ashley
729cb8fceb Create pv.css 2022-07-05 13:56:20 +03:00
Ashley
13df4b1df6 Update channel.ejs 2022-07-05 10:18:34 +03:00
Ashley
7d37da39c3 yo 2022-07-05 10:17:29 +03:00
Ashley
99fd4c3cba ch 2022-07-03 17:09:36 +03:00
Ashley
9f0e52e710 Rename t/staff/tube.svg to t/stuff/tube.svg 2022-07-03 16:27:49 +03:00
Ashley
9f32553014 Create tube.svg 2022-07-03 16:27:03 +03:00
Ashley
9619a1429b Create index.html 2022-07-03 16:25:30 +03:00
Ashley
1f1c516768 Update main.ejs 2022-07-03 16:24:20 +03:00
Ashley
d4f8114326 Create rep 2022-07-03 00:18:16 +03:00
Ashley
d2ccbb28be Create README.md 2022-07-03 00:17:32 +03:00
Ashley
30155a7051 Update poketube.ejs 2022-07-03 00:16:09 +03:00
Ashley
c3b0319a5b Add remembering technoblade 2022-07-01 19:26:34 +03:00
Ashley
749db8eab7 Epic:Add FA pro 2022-07-01 11:44:48 +03:00
Ashley
2a3b75013b Epic:Fix typo and add opt out for piwik 2022-06-30 00:17:54 +03:00
Ashley
db38766e5e Update poketube.ejs 2022-06-30 00:16:06 +03:00
Ashley
5705c45932 Update priv.ejs 2022-06-29 19:35:38 +03:00
Ashley
adba9632bd Epic:Add matomo
this allows us to see how much poketube users watched this video,we dont collect your ip or any of your personal information while making this. we dont collect or share your personal information as we said on our privacy policy:https://poketube.fun/privacy 

this process does not use cookies,and it does not collect any information whatsoever execpt how many users watched this spesific video 

we use something called matomo to do this,and since ip adreses are collected by def on matomo,we closed that on our instance. no infromation is collected from you. 

we dont collect or share your personal infromation,period.
2022-06-26 11:27:52 +03:00
Ashley
cdd62dc49d Epic:Add config object 2022-06-26 11:24:02 +03:00
Ashley
827f712ed6 new navbar 2022-06-26 08:05:06 +03:00
Ashley
f15398d5e2 Update priv.ejs 2022-06-26 08:04:40 +03:00
Ashley
60fed19775 new navbar 2022-06-26 08:03:53 +03:00
Ashley
d755ae74e8 new navbar 2022-06-26 08:03:24 +03:00
Ashley
68855652e4 new navbar 2022-06-26 08:01:44 +03:00
Ashley
5055906605 new navbar 2022-06-26 08:01:17 +03:00
Ashley
05d5b0e4b8 Update priv.ejs 2022-06-25 16:42:35 +03:00
Ashley
74b5c408f8 Update server.js 2022-06-25 12:04:01 +03:00
Ashley
5a5758c2b9 Update main.ejs 2022-06-25 12:03:29 +03:00
Ashley
010e1264ae Update channel.ejs 2022-06-25 12:02:49 +03:00
Ashley
c49878b2d7 Update search.ejs 2022-06-25 12:02:09 +03:00
Ashley
eec3509a1f Update poketube.ejs 2022-06-25 12:01:40 +03:00
Ashley
96e139ea58 Update yt-ukraine.svg 2022-06-25 11:59:05 +03:00
Ashley
9a5d6bb636 Create logo.svg 2022-06-25 11:58:30 +03:00
Ashley
e88c9356f3 Update poketube.ejs 2022-06-24 16:10:38 +03:00
Ashley
db2d419840 Update poketube.ejs 2022-06-24 13:55:55 +03:00
Ashley
ab8553a291 Update poketube.ejs 2022-06-24 13:55:31 +03:00
Ashley
130fc0163e Update poketube.ejs 2022-06-24 13:30:38 +03:00
Ashley
bfbca5d7bd Update watch.main.css 2022-06-24 12:25:51 +03:00
Ashley
b168a3677a Update search.ejs 2022-06-24 10:47:40 +03:00
Ashley
9415685b27 Update channel.ejs 2022-06-24 10:46:29 +03:00
Ashley
68f9acafbf Update poketube.ejs 2022-06-24 10:45:33 +03:00
Ashley
0900e8ae58 Update main.ejs 2022-06-23 18:39:09 +03:00
Ashley
1500b39acc Update poketube.ejs 2022-06-23 18:38:07 +03:00
Ashley
e2a241368b Update server.js 2022-06-23 18:36:44 +03:00
Ashley
6d26593e5b bye cache lol 2022-06-23 15:45:59 +03:00
Ashley
01ddc9bdf8 bye cache 2022-06-23 15:45:34 +03:00
Ashley
33d998ed9b bye cache 2022-06-23 15:45:09 +03:00
Ashley
9825d4cb9c Update 404.ejs 2022-06-23 15:44:30 +03:00
Ashley
5e18f2fdaa bye cache 2022-06-23 15:43:17 +03:00
Ashley
bf654fb970 Update server.js 2022-06-22 23:56:42 +03:00
Ashley
1fd520e55e Update channel.ejs 2022-06-22 23:56:02 +03:00
Ashley
afe5a0e5fd Update search.main.css 2022-06-22 23:28:25 +03:00
Ashley
9326b8e5e7 Update README.md 2022-06-22 22:18:57 +03:00
Ashley
a6a6006940 Update poketube.ejs 2022-06-22 17:55:30 +03:00
Ashley
4747d5c43c Update README.md 2022-06-22 17:43:32 +03:00
Ashley
29fb1b25b2 Update README.md 2022-06-22 17:43:10 +03:00
Ashley
22c7582bb6 Update poketube.ejs 2022-06-22 16:34:25 +03:00
Ashley
38bf5f3035 Update search.ejs 2022-06-22 16:33:55 +03:00
Ashley
a3e960c1e7 yo 2022-06-22 16:33:03 +03:00
Ashley
42ad36fc0b new url 2022-06-22 14:44:12 +03:00
Ashley
5b56e97eaa Update config.json 2022-06-22 14:43:49 +03:00
Ashley
cbfe540cdb Update poketube.ejs 2022-06-22 12:53:57 +03:00
Ashley
f56d1efca7 Create watch.main.css 2022-06-22 12:53:05 +03:00
Ashley
3509af6159 Create poketube-old.ejs 2022-06-22 12:52:26 +03:00
Ashley
f4eb4704e3 new ui pog 2022-06-22 12:52:00 +03:00
Ashley
244c904c7b new api url
lighttube.herokuapp.com --> tube.kuylar.dev
2022-06-20 12:00:10 +03:00
Ashley
b3f57a562d update the api url
lighttube.herokuapp.com --> tube.kuylar.dev
2022-06-20 11:58:59 +03:00
Ashley
0a23218c45 more stuff 2022-06-08 21:31:44 +03:00
Ashley
2d5e1a7b97 fixed some issues! 2022-06-08 21:31:07 +03:00
Ashley
84dc5f8d86 Update channels.ejs 2022-06-08 20:39:41 +03:00
Ashley
ea42e6bc24 channels lets go 2022-06-08 20:28:48 +03:00
Ashley
a9cc30b997 add query 2022-06-08 19:06:23 +03:00
Ashley
fc0fe67636 new ui pog 2022-06-08 19:05:47 +03:00
Ashley
09c50c0a32 Update search.ejs 2022-06-08 19:05:05 +03:00
Ashley
ec24c643b5 new! select search results 2022-06-08 17:13:19 +03:00
Ashley
660e2b9c15 Create search.main.css 2022-06-08 17:12:21 +03:00
Ashley
584ea806d3 new search! 2022-06-08 17:11:29 +03:00
Ashley
d3caf8bf93 better readability 2022-06-08 13:59:23 +03:00
Ashley
13982fdffa i forgor href 2022-06-04 22:01:29 +03:00
Ashley
b86e774722 Update and rename po.ejs to poketube.ejs 2022-06-04 19:36:54 +03:00
Ashley
a6314ddbbc Update and rename ytmain.ejs to main.ejs 2022-06-04 19:36:22 +03:00
Ashley
88e2ef3bd9 Create 143.ejs 2022-06-04 19:35:40 +03:00
Ashley
31ab1776f3 * Rename youtube.ejs and ytmain.ejs
and use video object instead
2022-06-04 19:35:11 +03:00
Ashley
a1e444654e use the glitch cdn instead of gstatic 2022-06-04 18:44:23 +03:00
Ashley
b65987589d Update README.md 2022-05-27 21:13:15 +03:00
Ashley
70d1c81d61 Update README.md 2022-05-27 21:08:24 +03:00
Ashley
61e8c00200 Update ytmain.ejs 2022-05-26 23:18:44 +03:00
Ashley
4594891abd Update youtube.ejs 2022-05-26 23:18:10 +03:00
Ashley
4fa4799bc4 Update domains.ejs 2022-05-26 23:16:39 +03:00
Ashley
25dc09a8a6 Update 404.ejs 2022-05-26 23:16:02 +03:00
Ashley
9386a68816 Update ytmain.ejs 2022-05-21 00:31:40 +03:00
Ashley
12b614db08 Update domains.ejs 2022-05-21 00:31:11 +03:00
Ashley
50c3ef9ab5 add more features 2022-05-21 00:30:32 +03:00
Ashley
72e676691e add color support! 2022-05-20 19:23:12 +03:00
Ashley
1da836b309 add color to embed! wooo 2022-05-20 19:22:27 +03:00
Ashley
d48f981b66 change packages n stuff 2022-05-20 19:21:45 +03:00
Ashley
8554208d45 remove LightTube from the home screen 2022-05-18 21:18:34 +03:00
Ashley
283b70968a *stuff smh 2022-05-18 21:17:08 +03:00
Ashley
d8f30d59e9 fix some stuf 2022-05-18 21:16:55 +03:00
Ashley
aaed77b062 HOLY FUCKING SHIT I HAD A TYPO ON A LEGAL DOCUMENT (AGAIN)
Pain
2022-05-18 20:33:34 +03:00
Ashley
75b6dbe11b Update README.md 2022-05-18 20:17:44 +03:00
Ashley
0cbb0b015b HOLY FUCKING SHIT I HAD A TYPO ON A LEGAL DOCUMENT 2022-05-18 20:04:06 +03:00
Ashley
b1d5f94185 new branding,again lol 2022-05-18 19:57:11 +03:00
Ashley
561b0af36a update privacy:
better readability lol
2022-05-18 19:48:29 +03:00
Ashley
6aa596f2a3 new branding 2022-05-18 19:47:16 +03:00
Ashley
f0122d934b new branding 2022-05-18 19:46:36 +03:00
Ashley
0a2c208f15 fix typo 2022-05-18 19:45:34 +03:00
Ashley
4ba4dd73a7 add new branding 2022-05-18 19:44:33 +03:00
Ashley
470a94a26c Update youtube.ejs 2022-05-17 23:21:29 +03:00
Ashley
b217719749 Update youtube.ejs 2022-05-17 23:20:49 +03:00
Ashley
93883c219d Update ytmain.ejs 2022-05-17 23:20:10 +03:00
Ashley
ca44e86512 new version pog 2022-05-17 23:19:35 +03:00
Ashley
68eee14649 new version! 2022-05-17 23:18:32 +03:00
Ashley
dbf91956f7 Update youtube.ejs 2022-05-17 23:08:31 +03:00
Ashley
eaa8bf991f Update ytmain.ejs 2022-05-12 19:51:28 +03:00
Ashley
2fa7357d74 fix typo 2022-05-03 19:58:16 +03:00
Ashley
fef5b95f61 Update server.js 2022-03-31 13:22:19 +03:00
Ashley
3125ccacf2 Update video.js 2022-03-31 13:21:14 +03:00
Ashley
8fa21c1ed9 Update README.md 2022-03-30 00:26:32 +03:00
Ashley
49d29ad2aa Update app-cdn.min.css 2022-03-27 12:46:11 +03:00
Ashley
ed43e1d791 Create app-cdn.min.css 2022-03-27 12:45:25 +03:00
Ashley
bea7d42375 made some changes 2022-03-27 12:44:25 +03:00
Ashley
673f70a8b9 Update server.js 2022-03-27 12:43:29 +03:00
Ashley
9f30d16b3c updated the main page 2022-03-27 09:54:00 +03:00
Ashley
e78350b64b fix an issue 2022-03-27 07:54:39 +03:00
Ashley
63362949bc remove /watchnew and add support for objects returned instead of arrays 2022-03-27 07:44:08 +03:00
Ashley
827a236b14 Update README.md 2022-03-26 13:13:44 +03:00
Ashley
b808caf3dd Update README.md 2022-03-26 13:04:40 +03:00
Ashley
ad8a6707bb Update server.js 2022-03-25 20:47:56 +03:00
Ashley
5698d435a7 Create search.js 2022-03-25 20:46:07 +03:00
Ashley
5d27724f34 Create README.md 2022-03-25 19:46:31 +03:00
Ashley
4b6010b8ff Create channel.js 2022-03-25 19:42:57 +03:00
Ashley
086d960f59 Create config.json 2022-03-25 19:42:30 +03:00
Ashley
3c149c0ea7 Create video.js 2022-03-25 19:42:11 +03:00
Ashley
9b62480b9f Create server.js 2022-03-25 19:41:48 +03:00
Ashley
6cb39b2ecf Create package.json 2022-03-25 19:40:20 +03:00
217 changed files with 46264 additions and 593 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
yarn.lock
package-lock.json
.env

147
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,147 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
iamashley@duck.com (E-mail) https://discord.gg/pfKSQ3pMfW (Discord server).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Additional Terms for poketube
1.Definitions
"alternative code of conduct" is a code of conduct that is not Contributor Covenant Code of Conduct.
"free software" is defined in the GNU GPL version 3. You can see a copy on the LICENSE file.
2.Terms
YOU MAY NOT EDIT, REMOVE, CHANGE THE TERMS OF THIS FILE. YOU MAY NOT
REMOVE THIS FILE FROM YOUR FORK OF POKETUBE in any way shape or form. Everyone is permitted to copy and distribute verbatim copies of this document, but changing it is not allowed. if you are using a older fork that doesnt have the Code of Conduct ( this FILE ) you dont have to accept this conduct. since poketube is free software, you can copy
and share copys under GPL-3.0-OR-LATER. if you dont want this conduct on your fork of poketube, you can use alternative
conducts instead. if you dont want the alternative code of conduct, you may con
tact us and explain why do you want to remove this file. we require this because
of the recent polymc event. (you can search it online about this event.) we are
sorry if this makes us a bad software, but we just dont want troll forks of poketube
to happen. poketube is NOT a political software, and we respect peoples opinions.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,23 +1,81 @@
[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
<a href="https://fsf.org">
<div align="center">
<a href="https://poketube.fun/watch?v=dQw4w9WgXcQ">
<img src="https://user-images.githubusercontent.com/65588168/156941082-2fe10d35-bc1d-4928-9c5d-32d91cdea3d8.png">
</a>
<h1> POKETUBE </h1>
youtube player that loves privacy
<br>
<img src="https://www.thinkpenguin.com/files/RYF.png" width="67" align="right" >
</a>
<div align="center">
<a href="https://poketube.fun/watch?v=9sJUDx7iEJw&quality=medium&=sjohgteojgytrueugtye4jhtytjrjnyıı">
<img src="https://poketube.fun/css/logo.svg" width="500"> </a><br>
<img src='https://raw.githubusercontent.com/squiresgrant/personal-site/main/badges/firefox4.gif'>
</div>
## Features
- Works on Older browsers!
- Javascript-free on frontend
- No advetisment
- See Dislike counts from [Return YouTube Dislike Api](https://www.returnyoutubedislike.com/) (see:[Line 22 of fetcher.js](https://github.com/iamashley0/poketube/blob/main/src/fetcher.js#L22))
- Uses LightTube api
tbanks to <a href="https://gitlab.com/kuylar/lighttube">LIGHTTUBE</a>
## Wats in dis readme uwu?:
- [Welcome!](#Welcome!)
- [No Non-free codec needed :3](#no-non-free-codec-needed-3)
- [Hosting Poketube~](#hosting-poketube)
- [PokeTube Offical Communitys](#poketube-community)
- [The Legal Stuff](#the-legal-stuff)
## Welcome!
This is the source code of PokeTube, the privacy-friendly youtube front-end built with the InnerTube API ([Docs](https://docs.poketube.fun)) that's packed with some pretty cool stuff including:
- ZERO ads
- Lyrics to songs
- A clean and modern UI
- A Javascript-free frontend
- No cookies or data collection
- And built-in dislike counts Thaks to the [Return YouTube Dislike Api](https://www.returnyoutubedislike.com/)!
## No Non-free codec needed :3
PokeTube uses openh264 which is free software! poketube does not inculude non free stuff owowowoow!!!!
you can view the source code of the openh264 codec in this repo :3 --> https://github.com/cisco/openh264.git
PLEASE NOTE THAT THIS SOFTWARE MAY INCULUDE CODECS THAT IN CERTAIN COUNTRIES MAY BE COVERED BY PATENTS OR HAVE LEGAL ISSUES. PATENT AND COPYRIGHT LAWS OPERATE DIFFERENTLY DEPENDING ON WHICH COUNTRY YOU ARE IN. PLEASE OBTAIN LEGAL ADVICE IF YOU ARE UNSURE WHETHER A PARTICULAR PATENT OR RESTRICTION APPLIES TO A CODEC YOU WISH TO USE IN YOUR COUNTRY.
## Hosting Poketube~
- To self host your own Poketube instance, you'll need the following:
- [Node.js](https://nodejs.org/en/download/)
- [npm](http://npmjs.com) (Included with Node.js)
Once you have everything, clone our repo:
```
git clone https://codeberg.org/ashley/poketube.git
```
You can also clone using our Github mirror if you'd prefer:
```
git clone https://github.com/ashley0143/poketube.git
```
Now, install the needed dependencies within the Poketube folder:
```
npm install
```
Once everythings installed, start your server with the following command:
```
node server.js
```
Congrats, Poketube should now be running on `localhost:3000`!
## PokeTube community!
Offical poketube community servers :3
[Revolt](https://rvlt.gg/1em7QW8C) <br>
[Discord](https://discord.gg/a3JFtTHUnp) (requires nonfree js - see stallman.org/discord.html )
## The Legal Stuff (boring tbh)
[Code Of conduct](https://codeberg.org/Ashley/poketube/src/branch/main/CODE_OF_CONDUCT.md) <br>
[Privacy Policy](https://poketube.fun/privacy) <br>
TL;DR: we dont collect or share your personal info, that's it lol
We additionally use the GNU Coding Standard, see https://www.gnu.org/prep/standards
<a href="https://codeberg.org/Ashley/poketube/src/branch/main/LICENSE"> <img src="https://www.gnu.org/graphics/gplv3-88x31.png"> </a>
#
<div align=center><img src="https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg"></div>

View File

@@ -0,0 +1,241 @@
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
Apple Lossless Format "Magic Cookie" Description
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
Many encoded formats for audio require additional, codec specific configuration information in order to operate successfully.
This codec specific information is often called a 'magic cookie'. The Apple Lossless codec's 'magic cookie' contains the
ALACSpecificConfig and optional ALACChannelLayoutInfo (both described below).
The 'magic cookie' must accompany the bitstream when stored in any file container (M4A/MP4, CAF) so that it may be provided to the
decoder when decoding the bitstream. From the caller's perspective, the 'magic cookie' is opaque and should be stored in the file
and presented to the decoder exactly as it is vended from the encoder (and consequently stored in the file).
The ALAC 'magic cookie' as stored in a file has all fields described in big-endian order (regardless of file format).
The layout of the 'magic cookie' is as follows:
---------------- ALAC Specific Info (24 bytes) (mandatory) ---------------------------
(ALACSpecificConfig) Decoder Info
---------------- Channel Layout Info (24 bytes) (optional) ----------------------------
(ALAC Channel Layout Info) Channel Layout Info
If the channel layout is absent from the cookie, then the following assumptions are made:
1 channel - mono
2 channels - stereo in left, right order
> 2 channels - no specific channel designation or role.
__________________________________________________________________________________________________________________________________
* ALAC Specific Info (24 bytes) (mandatory)
__________________________________________________________________________________________________________________________________
The Apple Lossless codec stores specific information about the encoded stream in the ALACSpecificConfig. This
info is vended by the encoder and is used to setup the decoder for a given encoded bitstream.
When read from and written to a file, the fields of this struct must be in big-endian order.
When vended by the encoder (and received by the decoder) the struct values will be in big-endian order.
/*
struct ALACSpecificConfig (defined in ALACAudioTypes.h)
abstract This struct is used to describe codec provided information about the encoded Apple Lossless bitstream.
It must accompany the encoded stream in the containing audio file and be provided to the decoder.
field frameLength uint32_t indicating the frames per packet when no explicit frames per packet setting is
present in the packet header. The encoder frames per packet can be explicitly set
but for maximum compatibility, the default encoder setting of 4096 should be used.
field compatibleVersion uint8_t indicating compatible version,
value must be set to 0
field bitDepth uint8_t describes the bit depth of the source PCM data (maximum value = 32)
field pb uint8_t currently unused tuning parameter.
value should be set to 40
field mb uint8_t currently unused tuning parameter.
value should be set to 10
field kb uint8_t currently unused tuning parameter.
value should be set to 14
field numChannels uint8_t describes the channel count (1 = mono, 2 = stereo, etc...)
when channel layout info is not provided in the 'magic cookie', a channel count > 2
describes a set of discreet channels with no specific ordering
field maxRun uint16_t currently unused.
value should be set to 255
field maxFrameBytes uint32_t the maximum size of an Apple Lossless packet within the encoded stream.
value of 0 indicates unknown
field avgBitRate uint32_t the average bit rate in bits per second of the Apple Lossless stream.
value of 0 indicates unknown
field sampleRate uint32_t sample rate of the encoded stream
*/
typedef struct ALACSpecificConfig
{
uint32_t frameLength;
uint8_t compatibleVersion;
uint8_t bitDepth;
uint8_t pb;
uint8_t mb;
uint8_t kb;
uint8_t numChannels;
uint16_t maxRun;
uint32_t maxFrameBytes;
uint32_t avgBitRate;
uint32_t sampleRate;
} ALACSpecificConfig;
__________________________________________________________________________________________________________________________________
Channel Layout Info (24 bytes) (optional)
__________________________________________________________________________________________________________________________________
The Apple Lossless codec can support a specific set of channel layouts. When channel information is vended
by the encoder (in the 'magic cookie'), it is formatted in the the ALACChannelLayoutInfo.
When read from and written to a file, the fields of this struct must be in big-endian order.
When vended by the encoder (and received by the decoder) the struct values will be in big-endian order.
/*
struct ALACChannelLayoutInfo (defined in ALACAudioTypes.h)
abstract This struct is used to specify particular channel orderings or configurations.
It is an optional portion of the 'magic cookie', being required to describe specific channel layouts (see below)
of more than 2 channels.
field channelLayoutInfoSize uint32_t indicates the size of the channel layout data
value should be set to 24
field channelLayoutInfoID uint32_t identifier indicating that channel layout info is present
value = 'chan'
field versionFlags uint32_t version flags
value should be set to 0
field channelLayoutTag uint32_t channel layout type
from defined list in ALACAudioTypes.h (see below)
field reserved1 uint32_t currently unused field
value should be set to 0
field reserved2 uint32_t currently unused field
value should be set to 0
*/
typedef struct ALACChannelLayoutInfo
{
uint32_t channelLayoutInfoSize;
uint32_t channelLayoutInfoID;
uint32_t versionFlags;
uint32_t channelLayoutTag;
uint32_t reserved1;
uint32_t reserved2;
} ALACChannelLayoutInfo;
* Channel Layout Tags
These constants will be used to describe the bitstream's channel layout. (defined in ALACAudioTypes.h)
enum
{
kALACChannelLayoutTag_Mono = (100<<16) | 1, // C
kALACChannelLayoutTag_Stereo = (101<<16) | 2, // L R
kALACChannelLayoutTag_MPEG_3_0_B = (113<<16) | 3, // C L R
kALACChannelLayoutTag_MPEG_4_0_B = (116<<16) | 4, // C L R Cs
kALACChannelLayoutTag_MPEG_5_0_D = (120<<16) | 5, // C L R Ls Rs
kALACChannelLayoutTag_MPEG_5_1_D = (124<<16) | 6, // C L R Ls Rs LFE
kALACChannelLayoutTag_AAC_6_1 = (142<<16) | 7, // C L R Ls Rs Cs LFE
kALACChannelLayoutTag_MPEG_7_1_B = (127<<16) | 8 // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
};
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
* Storing Apple Lossless Magic Cookie in Audio Files
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
The Apple Lossless Magic Cookie is treated as opaque by file parsing code. The 'magic cookie' vended by the encoder
is placed without modification into the audio file and the read from that file and passed (unmodified) to the decoder.
__________________________________________________________________________________________________________________________________
* CAF File
In a CAF file (Core Audio File), the 'magic cookie' is stored in CAF's Magic Cookie chunk ('kuki').
__________________________________________________________________________________________________________________________________
* MP4/M4A File
In an MP4/M4A file, the 'magic cookie' is encapsulated in the AudioSample entry of a Sound Description box ('stsd').
An ISO style full box header to describe the ALACSpecificConfig portion is appended to the AudioSampleEntry, followed by the
'magic cookie' as it is vended by the encoder.
(All fields are stored in big-endian order: see ISO/IEC 14496-12 for a full description of the SoundDescription and AudioSampleEntry boxes, etc.)
---------------- SoundDescriptionBox (FullBox) ----------------------------
SampleEntrySize // = sizeof(SoundDescriptionBox)(16) + sizeof (AudioSampleEntry)(AudioSampleEntry.SampleEntrySize)
SampleEntryType // = 'stsd'
VersionFlags // = 0
EntryCount // = 1
---------------- Audio Sample Entry (REQUIRED) -----------------------------
SampleEntrySize // sizeof(AudioSampleEntry)(36) + sizeof(full ISO box header)(12) + sizeof(Apple Lossless Magic Cookie)
SampleEntryType // = 'alac', specifies that the AudioSampleEntry describes an Apple Lossless bitstream
mReserved[6] // = 0
dref index // = 1
reserved[2] // = 0
channel count // = number of channels as a uint_16 value
sample size // = source pcm bitdepth (example = 16bit source pcm)
predefined // = 0
reserved // = 0
sample rate // sample rate as a uint_32 value
Appended to AudioSampleEntry:
ALAC Specific Info Size // uint_32 value, = 36 (12 + sizeof(ALACSpecificConfig))
ALAC Specific Info ID // uint_32 value, = 'alac', format ID which matches the Audio Sample Entry SampleEntryType field
Version Flags // uint_32 value, = 0
Apple Lossless Magic Cookie // 'magic cookie' vended from ALAC encoder (24 or 48 Bytes)
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
* Compatibility
__________________________________________________________________________________________________________________________________
__________________________________________________________________________________________________________________________________
Previous versions of the Apple Lossless encoder vended a different 'magic cookie'. To ensure compatibility, the Apple Lossless decoder
must be prepared to parse a 'magic cookie' in the format described below. Note that the 'magic cookie' defined above is
encapsulated in the following method and can be extracted as a contiguous set of bytes.
---------------- Format Atom (12 bytes) --------------------------------
(uint_32) Format Atom Size // = 12
(uint_32) Channel Layout Info ID // = 'frma'
(uint_32) Format Type // = 'alac'
---------------- ALAC Specific Info (36 bytes) (required) --------------
(uint_32) ALAC Specific Info Size // = 36 (12 + sizeof(ALACSpecificConfig))
(uint_32) ALAC Specific Info ID // = 'alac', format ID which matches the Audio Sample Entry SampleEntryType field
(uint_32) Version Flags // = 0
[ Apple Lossless Magic Cookie (see above) ]
---------------- Terminator Atom (8 bytes) -----------------------------
(uint_32) Channel Layout Info Size // = 8
(uint_32) Channel Layout Info ID // = 0

53
alac/LICENSE Normal file
View File

@@ -0,0 +1,53 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

44
alac/ReadMe.txt Normal file
View File

@@ -0,0 +1,44 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The Apple Lossless Format
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Apple Lossless supports the following features. Not all of these are implemented in alacconvert, though they are in the codec code provided.
1. Bit depths 16, 20, 24 and 32 bits.
2. Any arbitrary integer sample rate from 1 to 384,000 Hz. In theory rates up to 4,294,967,295 (2^32 - 1) Hz could be supported.
3. From one to eight channels are supported. Channel orders for the supported formats are described as:
Num Chan Order
1 mono
2 stereo (Left, Right)
3 MPEG 3.0 B (Center, Left, Right)
4 MPEG 4.0 B (Center, Left, Right, Center Surround)
5 MPEG 5.0 D (Center, Left, Right, Left Surround, Right Surround)
6 MPEG 5.1 D (Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
7 Apple AAC 6.1 (Center, Left, Right, Left Surround, Right Surround, Center Surround, Low Frequency Effects)
8 MPEG 7.1 B (Center, Left Center, Right Center, Left, Right, Left Surround, Right Surround, Low Frequency Effects)
4. Packet size defaults to 4096 sample frames of audio per packet. Other packet sizes are certainly possible. However, non-default packet sizes are not guaranteed to work properly on all hardware devices that support Apple Lossless. Packets above 16,384 sample frames are not supported.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
This package contains the sources for the Apple Lossless (ALAC) encoder and decoder.
The "codec" directory contains all the sources necessary for a functioning codec. Also includes is a makefile that will build libalac.a on a UNIX/Linux machine.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ALACconvert
The convert-utility directory contains sources to build alacconvert which is a simple utility that demonstrates how to use the included ALAC encoder and decoder.
alacconvert supports the following formats:
1. 16- or 24-bit mono or stereo .wav files where the data is little endian integer. Extended WAVE format chunks are not handled.
2. 16- or 24-bit mono or stereo .caf (Core Audio Format) files as well as certain multi-channel configurations where the data is big or little endian integer. It does no channel order manipulation.
3. ALAC .caf files.
Three project are provided to build a command line utility called alacconvert that converts cpm data to ALAC or vice versa. A Mac OS X Xcode project, A Windows Visual Studio project, and a generic UNIX/Linux make file.
Note: When building on Windows, if you are using a version of Visual Studio before Visual Studio 2010, <stdint.h> is not installed. You will need to acquire this file on your own. It can be put in the same directory as the project.

197
alac/codec/ALACAudioTypes.h Normal file
View File

@@ -0,0 +1,197 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: ALACAudioTypes.h
*/
#ifndef ALACAUDIOTYPES_H
#define ALACAUDIOTYPES_H
#if PRAGMA_ONCE
#pragma once
#endif
#ifdef __cplusplus
extern "C" {
#endif
#if PRAGMA_STRUCT_ALIGN
#pragma options align=mac68k
#elif PRAGMA_STRUCT_PACKPUSH
#pragma pack(push, 2)
#elif PRAGMA_STRUCT_PACK
#pragma pack(2)
#endif
#include <stdint.h>
#if defined(__ppc__)
#define TARGET_RT_BIG_ENDIAN 1
#elif defined(__ppc64__)
#define TARGET_RT_BIG_ENDIAN 1
#endif
#define kChannelAtomSize 12
enum
{
kALAC_UnimplementedError = -4,
kALAC_FileNotFoundError = -43,
kALAC_ParamError = -50,
kALAC_MemFullError = -108
};
enum
{
kALACFormatAppleLossless = 'alac',
kALACFormatLinearPCM = 'lpcm'
};
enum
{
kALACMaxChannels = 8,
kALACMaxEscapeHeaderBytes = 8,
kALACMaxSearches = 16,
kALACMaxCoefs = 16,
kALACDefaultFramesPerPacket = 4096
};
typedef uint32_t ALACChannelLayoutTag;
enum
{
kALACFormatFlagIsFloat = (1 << 0), // 0x1
kALACFormatFlagIsBigEndian = (1 << 1), // 0x2
kALACFormatFlagIsSignedInteger = (1 << 2), // 0x4
kALACFormatFlagIsPacked = (1 << 3), // 0x8
kALACFormatFlagIsAlignedHigh = (1 << 4), // 0x10
};
enum
{
#if TARGET_RT_BIG_ENDIAN
kALACFormatFlagsNativeEndian = kALACFormatFlagIsBigEndian
#else
kALACFormatFlagsNativeEndian = 0
#endif
};
// this is required to be an IEEE 64bit float
typedef double alac_float64_t;
// These are the Channel Layout Tags used in the Channel Layout Info portion of the ALAC magic cookie
enum
{
kALACChannelLayoutTag_Mono = (100<<16) | 1, // C
kALACChannelLayoutTag_Stereo = (101<<16) | 2, // L R
kALACChannelLayoutTag_MPEG_3_0_B = (113<<16) | 3, // C L R
kALACChannelLayoutTag_MPEG_4_0_B = (116<<16) | 4, // C L R Cs
kALACChannelLayoutTag_MPEG_5_0_D = (120<<16) | 5, // C L R Ls Rs
kALACChannelLayoutTag_MPEG_5_1_D = (124<<16) | 6, // C L R Ls Rs LFE
kALACChannelLayoutTag_AAC_6_1 = (142<<16) | 7, // C L R Ls Rs Cs LFE
kALACChannelLayoutTag_MPEG_7_1_B = (127<<16) | 8 // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
};
// ALAC currently only utilizes these channels layouts. There is a one for one correspondance between a
// given number of channels and one of these layout tags
static const ALACChannelLayoutTag ALACChannelLayoutTags[kALACMaxChannels] =
{
kALACChannelLayoutTag_Mono, // C
kALACChannelLayoutTag_Stereo, // L R
kALACChannelLayoutTag_MPEG_3_0_B, // C L R
kALACChannelLayoutTag_MPEG_4_0_B, // C L R Cs
kALACChannelLayoutTag_MPEG_5_0_D, // C L R Ls Rs
kALACChannelLayoutTag_MPEG_5_1_D, // C L R Ls Rs LFE
kALACChannelLayoutTag_AAC_6_1, // C L R Ls Rs Cs LFE
kALACChannelLayoutTag_MPEG_7_1_B // C Lc Rc L R Ls Rs LFE (doc: IS-13818-7 MPEG2-AAC)
};
// AudioChannelLayout from CoreAudioTypes.h. We never need the AudioChannelDescription so we remove it
struct ALACAudioChannelLayout
{
ALACChannelLayoutTag mChannelLayoutTag;
uint32_t mChannelBitmap;
uint32_t mNumberChannelDescriptions;
};
typedef struct ALACAudioChannelLayout ALACAudioChannelLayout;
struct AudioFormatDescription
{
alac_float64_t mSampleRate;
uint32_t mFormatID;
uint32_t mFormatFlags;
uint32_t mBytesPerPacket;
uint32_t mFramesPerPacket;
uint32_t mBytesPerFrame;
uint32_t mChannelsPerFrame;
uint32_t mBitsPerChannel;
uint32_t mReserved;
};
typedef struct AudioFormatDescription AudioFormatDescription;
/* Lossless Definitions */
enum
{
kALACCodecFormat = 'alac',
kALACVersion = 0,
kALACCompatibleVersion = kALACVersion,
kALACDefaultFrameSize = 4096
};
// note: this struct is wrapped in an 'alac' atom in the sample description extension area
// note: in QT movies, it will be further wrapped in a 'wave' atom surrounded by 'frma' and 'term' atoms
typedef struct ALACSpecificConfig
{
uint32_t frameLength;
uint8_t compatibleVersion;
uint8_t bitDepth; // max 32
uint8_t pb; // 0 <= pb <= 255
uint8_t mb;
uint8_t kb;
uint8_t numChannels;
uint16_t maxRun;
uint32_t maxFrameBytes;
uint32_t avgBitRate;
uint32_t sampleRate;
} ALACSpecificConfig;
// The AudioChannelLayout atom type is not exposed yet so define it here
enum
{
AudioChannelLayoutAID = 'chan'
};
#if PRAGMA_STRUCT_ALIGN
#pragma options align=reset
#elif PRAGMA_STRUCT_PACKPUSH
#pragma pack(pop)
#elif PRAGMA_STRUCT_PACK
#pragma pack()
#endif
#ifdef __cplusplus
}
#endif
#endif /* ALACAUDIOTYPES_H */

View File

@@ -0,0 +1,260 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*=============================================================================
File: ALACBitUtilities.c
$NoKeywords: $
=============================================================================*/
#include <stdio.h>
#include "ALACBitUtilities.h"
// BitBufferInit
//
void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize )
{
bits->cur = buffer;
bits->end = bits->cur + byteSize;
bits->bitIndex = 0;
bits->byteSize = byteSize;
}
// BitBufferRead
//
uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits )
{
uint32_t returnBits;
//Assert( numBits <= 16 );
returnBits = ((uint32_t)bits->cur[0] << 16) | ((uint32_t)bits->cur[1] << 8) | ((uint32_t)bits->cur[2]);
returnBits = returnBits << bits->bitIndex;
returnBits &= 0x00FFFFFF;
bits->bitIndex += numBits;
returnBits = returnBits >> (24 - numBits);
bits->cur += (bits->bitIndex >> 3);
bits->bitIndex &= 7;
//Assert( bits->cur <= bits->end );
return returnBits;
}
// BitBufferReadSmall
//
// Reads up to 8 bits
uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits )
{
uint16_t returnBits;
//Assert( numBits <= 8 );
returnBits = (bits->cur[0] << 8) | bits->cur[1];
returnBits = returnBits << bits->bitIndex;
bits->bitIndex += numBits;
returnBits = returnBits >> (16 - numBits);
bits->cur += (bits->bitIndex >> 3);
bits->bitIndex &= 7;
//Assert( bits->cur <= bits->end );
return (uint8_t)returnBits;
}
// BitBufferReadOne
//
// Reads one byte
uint8_t BitBufferReadOne( BitBuffer * bits )
{
uint8_t returnBits;
returnBits = (bits->cur[0] >> (7 - bits->bitIndex)) & 1;
bits->bitIndex++;
bits->cur += (bits->bitIndex >> 3);
bits->bitIndex &= 7;
//Assert( bits->cur <= bits->end );
return returnBits;
}
// BitBufferPeek
//
uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits )
{
return ((((((uint32_t) bits->cur[0] << 16) | ((uint32_t) bits->cur[1] << 8) |
((uint32_t) bits->cur[2])) << bits->bitIndex) & 0x00FFFFFF) >> (24 - numBits));
}
// BitBufferPeekOne
//
uint32_t BitBufferPeekOne( BitBuffer * bits )
{
return ((bits->cur[0] >> (7 - bits->bitIndex)) & 1);
}
// BitBufferUnpackBERSize
//
uint32_t BitBufferUnpackBERSize( BitBuffer * bits )
{
uint32_t size;
uint8_t tmp;
for ( size = 0, tmp = 0x80u; tmp &= 0x80u; size = (size << 7u) | (tmp & 0x7fu) )
tmp = (uint8_t) BitBufferReadSmall( bits, 8 );
return size;
}
// BitBufferGetPosition
//
uint32_t BitBufferGetPosition( BitBuffer * bits )
{
uint8_t * begin;
begin = bits->end - bits->byteSize;
return ((uint32_t)(bits->cur - begin) * 8) + bits->bitIndex;
}
// BitBufferByteAlign
//
void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros )
{
// align bit buffer to next byte boundary, writing zeros if requested
if ( bits->bitIndex == 0 )
return;
if ( addZeros )
BitBufferWrite( bits, 0, 8 - bits->bitIndex );
else
BitBufferAdvance( bits, 8 - bits->bitIndex );
}
// BitBufferAdvance
//
void BitBufferAdvance( BitBuffer * bits, uint32_t numBits )
{
if ( numBits )
{
bits->bitIndex += numBits;
bits->cur += (bits->bitIndex >> 3);
bits->bitIndex &= 7;
}
}
// BitBufferRewind
//
void BitBufferRewind( BitBuffer * bits, uint32_t numBits )
{
uint32_t numBytes;
if ( numBits == 0 )
return;
if ( bits->bitIndex >= numBits )
{
bits->bitIndex -= numBits;
return;
}
numBits -= bits->bitIndex;
bits->bitIndex = 0;
numBytes = numBits / 8;
numBits = numBits % 8;
bits->cur -= numBytes;
if ( numBits > 0 )
{
bits->bitIndex = 8 - numBits;
bits->cur--;
}
if ( bits->cur < (bits->end - bits->byteSize) )
{
//DebugCMsg("BitBufferRewind: Rewound too far.");
bits->cur = (bits->end - bits->byteSize);
bits->bitIndex = 0;
}
}
// BitBufferWrite
//
void BitBufferWrite( BitBuffer * bits, uint32_t bitValues, uint32_t numBits )
{
uint32_t invBitIndex;
RequireAction( bits != nil, return; );
RequireActionSilent( numBits > 0, return; );
invBitIndex = 8 - bits->bitIndex;
while ( numBits > 0 )
{
uint32_t tmp;
uint8_t shift;
uint8_t mask;
uint32_t curNum;
curNum = MIN( invBitIndex, numBits );
tmp = bitValues >> (numBits - curNum);
shift = (uint8_t)(invBitIndex - curNum);
mask = 0xffu >> (8 - curNum); // must be done in two steps to avoid compiler sequencing ambiguity
mask <<= shift;
bits->cur[0] = (bits->cur[0] & ~mask) | (((uint8_t) tmp << shift) & mask);
numBits -= curNum;
// increment to next byte if need be
invBitIndex -= curNum;
if ( invBitIndex == 0 )
{
invBitIndex = 8;
bits->cur++;
}
}
bits->bitIndex = 8 - invBitIndex;
}
void BitBufferReset( BitBuffer * bits )
//void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize )
{
bits->cur = bits->end - bits->byteSize;
bits->bitIndex = 0;
}
#if PRAGMA_MARK
#pragma mark -
#endif

View File

@@ -0,0 +1,104 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*=============================================================================
File: ALACBitUtilities.h
$NoKeywords: $
=============================================================================*/
#ifndef __ALACBITUTILITIES_H
#define __ALACBITUTILITIES_H
#include <stdint.h>
#ifndef MIN
#define MIN(x, y) ( (x)<(y) ?(x) :(y) )
#endif //MIN
#ifndef MAX
#define MAX(x, y) ( (x)>(y) ?(x): (y) )
#endif //MAX
#ifndef nil
#define nil NULL
#endif
#define RequireAction(condition, action) if (!(condition)) { action }
#define RequireActionSilent(condition, action) if (!(condition)) { action }
#define RequireNoErr(condition, action) if ((condition)) { action }
#ifdef __cplusplus
extern "C" {
#endif
enum
{
ALAC_noErr = 0
};
typedef enum
{
ID_SCE = 0, /* Single Channel Element */
ID_CPE = 1, /* Channel Pair Element */
ID_CCE = 2, /* Coupling Channel Element */
ID_LFE = 3, /* LFE Channel Element */
ID_DSE = 4, /* not yet supported */
ID_PCE = 5,
ID_FIL = 6,
ID_END = 7
} ELEMENT_TYPE;
// types
typedef struct BitBuffer
{
uint8_t * cur;
uint8_t * end;
uint32_t bitIndex;
uint32_t byteSize;
} BitBuffer;
/*
BitBuffer routines
- these routines take a fixed size buffer and read/write to it
- bounds checking must be done by the client
*/
void BitBufferInit( BitBuffer * bits, uint8_t * buffer, uint32_t byteSize );
uint32_t BitBufferRead( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time
uint8_t BitBufferReadSmall( BitBuffer * bits, uint8_t numBits );
uint8_t BitBufferReadOne( BitBuffer * bits );
uint32_t BitBufferPeek( BitBuffer * bits, uint8_t numBits ); // note: cannot read more than 16 bits at a time
uint32_t BitBufferPeekOne( BitBuffer * bits );
uint32_t BitBufferUnpackBERSize( BitBuffer * bits );
uint32_t BitBufferGetPosition( BitBuffer * bits );
void BitBufferByteAlign( BitBuffer * bits, int32_t addZeros );
void BitBufferAdvance( BitBuffer * bits, uint32_t numBits );
void BitBufferRewind( BitBuffer * bits, uint32_t numBits );
void BitBufferWrite( BitBuffer * bits, uint32_t value, uint32_t numBits );
void BitBufferReset( BitBuffer * bits);
#ifdef __cplusplus
}
#endif
#endif /* __BITUTILITIES_H */

730
alac/codec/ALACDecoder.cpp Normal file
View File

@@ -0,0 +1,730 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: ALACDecoder.cpp
*/
#include <stdlib.h>
#include <string.h>
#include "ALACDecoder.h"
#include "dplib.h"
#include "aglib.h"
#include "matrixlib.h"
#include "ALACBitUtilities.h"
#include "EndianPortable.h"
// constants/data
const uint32_t kMaxBitDepth = 32; // max allowed bit depth is 32
// prototypes
static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride );
static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride );
static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride );
/*
Constructor
*/
ALACDecoder::ALACDecoder() :
mMixBufferU( nil ),
mMixBufferV( nil ),
mPredictor( nil ),
mShiftBuffer( nil )
{
memset( &mConfig, 0, sizeof(mConfig) );
}
/*
Destructor
*/
ALACDecoder::~ALACDecoder()
{
// delete the matrix mixing buffers
if ( mMixBufferU )
{
free(mMixBufferU);
mMixBufferU = NULL;
}
if ( mMixBufferV )
{
free(mMixBufferV);
mMixBufferV = NULL;
}
// delete the dynamic predictor's "corrector" buffer
// - note: mShiftBuffer shares memory with this buffer
if ( mPredictor )
{
free(mPredictor);
mPredictor = NULL;
}
}
/*
Init()
- initialize the decoder with the given configuration
*/
int32_t ALACDecoder::Init( void * inMagicCookie, uint32_t inMagicCookieSize )
{
int32_t status = ALAC_noErr;
ALACSpecificConfig theConfig;
uint8_t * theActualCookie = (uint8_t *)inMagicCookie;
uint32_t theCookieBytesRemaining = inMagicCookieSize;
// For historical reasons the decoder needs to be resilient to magic cookies vended by older encoders.
// As specified in the ALACMagicCookieDescription.txt document, there may be additional data encapsulating
// the ALACSpecificConfig. This would consist of format ('frma') and 'alac' atoms which precede the
// ALACSpecificConfig.
// See ALACMagicCookieDescription.txt for additional documentation concerning the 'magic cookie'
// skip format ('frma') atom if present
if (theActualCookie[4] == 'f' && theActualCookie[5] == 'r' && theActualCookie[6] == 'm' && theActualCookie[7] == 'a')
{
theActualCookie += 12;
theCookieBytesRemaining -= 12;
}
// skip 'alac' atom header if present
if (theActualCookie[4] == 'a' && theActualCookie[5] == 'l' && theActualCookie[6] == 'a' && theActualCookie[7] == 'c')
{
theActualCookie += 12;
theCookieBytesRemaining -= 12;
}
// read the ALACSpecificConfig
if (theCookieBytesRemaining >= sizeof(ALACSpecificConfig))
{
theConfig.frameLength = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->frameLength);
theConfig.compatibleVersion = ((ALACSpecificConfig *)theActualCookie)->compatibleVersion;
theConfig.bitDepth = ((ALACSpecificConfig *)theActualCookie)->bitDepth;
theConfig.pb = ((ALACSpecificConfig *)theActualCookie)->pb;
theConfig.mb = ((ALACSpecificConfig *)theActualCookie)->mb;
theConfig.kb = ((ALACSpecificConfig *)theActualCookie)->kb;
theConfig.numChannels = ((ALACSpecificConfig *)theActualCookie)->numChannels;
theConfig.maxRun = Swap16BtoN(((ALACSpecificConfig *)theActualCookie)->maxRun);
theConfig.maxFrameBytes = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->maxFrameBytes);
theConfig.avgBitRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->avgBitRate);
theConfig.sampleRate = Swap32BtoN(((ALACSpecificConfig *)theActualCookie)->sampleRate);
mConfig = theConfig;
RequireAction( mConfig.compatibleVersion <= kALACVersion, return kALAC_ParamError; );
// allocate mix buffers
mMixBufferU = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
mMixBufferV = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
// allocate dynamic predictor buffer
mPredictor = (int32_t *) calloc( mConfig.frameLength * sizeof(int32_t), 1 );
// "shift off" buffer shares memory with predictor buffer
mShiftBuffer = (uint16_t *) mPredictor;
RequireAction( (mMixBufferU != nil) && (mMixBufferV != nil) && (mPredictor != nil),
status = kALAC_MemFullError; goto Exit; );
}
else
{
status = kALAC_ParamError;
}
// skip to Channel Layout Info
// theActualCookie += sizeof(ALACSpecificConfig);
// Currently, the Channel Layout Info portion of the magic cookie (as defined in the
// ALACMagicCookieDescription.txt document) is unused by the decoder.
Exit:
return status;
}
/*
Decode()
- the decoded samples are interleaved into the output buffer in the order they arrive in
the bitstream
*/
int32_t ALACDecoder::Decode( BitBuffer * bits, uint8_t * sampleBuffer, uint32_t numSamples, uint32_t numChannels, uint32_t * outNumSamples )
{
BitBuffer shiftBits;
uint32_t bits1, bits2;
uint8_t tag;
uint8_t elementInstanceTag;
AGParamRec agParams;
uint32_t channelIndex;
int16_t coefsU[32]; // max possible size is 32 although NUMCOEPAIRS is the current limit
int16_t coefsV[32];
uint8_t numU, numV;
uint8_t mixBits;
int8_t mixRes;
uint16_t unusedHeader;
uint8_t escapeFlag;
uint32_t chanBits;
uint8_t bytesShifted;
uint32_t shift;
uint8_t modeU, modeV;
uint32_t denShiftU, denShiftV;
uint16_t pbFactorU, pbFactorV;
uint16_t pb;
int16_t * samples;
int16_t * out16;
uint8_t * out20;
uint8_t * out24;
int32_t * out32;
uint8_t headerByte;
uint8_t partialFrame;
uint32_t extraBits;
int32_t val;
uint32_t i, j;
int32_t status;
RequireAction( (bits != nil) && (sampleBuffer != nil) && (outNumSamples != nil), return kALAC_ParamError; );
RequireAction( numChannels > 0, return kALAC_ParamError; );
mActiveElements = 0;
channelIndex = 0;
samples = (int16_t *) sampleBuffer;
status = ALAC_noErr;
*outNumSamples = numSamples;
while ( status == ALAC_noErr )
{
// bail if we ran off the end of the buffer
RequireAction( bits->cur < bits->end, status = kALAC_ParamError; goto Exit; );
// copy global decode params for this element
pb = mConfig.pb;
// read element tag
tag = BitBufferReadSmall( bits, 3 );
switch ( tag )
{
case ID_SCE:
case ID_LFE:
{
// mono/LFE channel
elementInstanceTag = BitBufferReadSmall( bits, 4 );
mActiveElements |= (1u << elementInstanceTag);
// read the 12 unused header bits
unusedHeader = (uint16_t) BitBufferRead( bits, 12 );
RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; );
// read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag
headerByte = (uint8_t) BitBufferRead( bits, 4 );
partialFrame = headerByte >> 3;
bytesShifted = (headerByte >> 1) & 0x3u;
RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; );
shift = bytesShifted * 8;
escapeFlag = headerByte & 0x1;
chanBits = mConfig.bitDepth - (bytesShifted * 8);
// check for partial frame to override requested numSamples
if ( partialFrame != 0 )
{
numSamples = BitBufferRead( bits, 16 ) << 16;
numSamples |= BitBufferRead( bits, 16 );
}
if ( escapeFlag == 0 )
{
// compressed frame, read rest of parameters
mixBits = (uint8_t) BitBufferRead( bits, 8 );
mixRes = (int8_t) BitBufferRead( bits, 8 );
//Assert( (mixBits == 0) && (mixRes == 0) ); // no mixing for mono
headerByte = (uint8_t) BitBufferRead( bits, 8 );
modeU = headerByte >> 4;
denShiftU = headerByte & 0xfu;
headerByte = (uint8_t) BitBufferRead( bits, 8 );
pbFactorU = headerByte >> 5;
numU = headerByte & 0x1fu;
for ( i = 0; i < numU; i++ )
coefsU[i] = (int16_t) BitBufferRead( bits, 16 );
// if shift active, skip the the shift buffer but remember where it starts
if ( bytesShifted != 0 )
{
shiftBits = *bits;
BitBufferAdvance( bits, (bytesShifted * 8) * numSamples );
}
// decompress
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 );
RequireNoErr( status, goto Exit; );
if ( modeU == 0 )
{
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
}
else
{
// the special "numActive == 31" mode can be done in-place
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
}
}
else
{
//Assert( bytesShifted == 0 );
// uncompressed frame, copy data into the mix buffer to use common output code
shift = 32 - chanBits;
if ( chanBits <= 16 )
{
for ( i = 0; i < numSamples; i++ )
{
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
val = (val << shift) >> shift;
mMixBufferU[i] = val;
}
}
else
{
// BitBufferRead() can't read more than 16 bits at a time so break up the reads
extraBits = chanBits - 16;
for ( i = 0; i < numSamples; i++ )
{
val = (int32_t) BitBufferRead( bits, 16 );
val = (val << 16) >> shift;
mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t) extraBits );
}
}
mixBits = mixRes = 0;
bits1 = chanBits * numSamples;
bytesShifted = 0;
}
// now read the shifted values into the shift buffer
if ( bytesShifted != 0 )
{
shift = bytesShifted * 8;
//Assert( shift <= 16 );
for ( i = 0; i < numSamples; i++ )
mShiftBuffer[i] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
}
// convert 32-bit integers into output buffer
switch ( mConfig.bitDepth )
{
case 16:
out16 = &((int16_t *)sampleBuffer)[channelIndex];
for ( i = 0, j = 0; i < numSamples; i++, j += numChannels )
out16[j] = (int16_t) mMixBufferU[i];
break;
case 20:
out20 = (uint8_t *)sampleBuffer + (channelIndex * 3);
copyPredictorTo20( mMixBufferU, out20, numChannels, numSamples );
break;
case 24:
out24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
if ( bytesShifted != 0 )
copyPredictorTo24Shift( mMixBufferU, mShiftBuffer, out24, numChannels, numSamples, bytesShifted );
else
copyPredictorTo24( mMixBufferU, out24, numChannels, numSamples );
break;
case 32:
out32 = &((int32_t *)sampleBuffer)[channelIndex];
if ( bytesShifted != 0 )
copyPredictorTo32Shift( mMixBufferU, mShiftBuffer, out32, numChannels, numSamples, bytesShifted );
else
copyPredictorTo32( mMixBufferU, out32, numChannels, numSamples);
break;
}
channelIndex += 1;
*outNumSamples = numSamples;
break;
}
case ID_CPE:
{
// if decoding this pair would take us over the max channels limit, bail
if ( (channelIndex + 2) > numChannels )
goto NoMoreChannels;
// stereo channel pair
elementInstanceTag = BitBufferReadSmall( bits, 4 );
mActiveElements |= (1u << elementInstanceTag);
// read the 12 unused header bits
unusedHeader = (uint16_t) BitBufferRead( bits, 12 );
RequireAction( unusedHeader == 0, status = kALAC_ParamError; goto Exit; );
// read the 1-bit "partial frame" flag, 2-bit "shift-off" flag & 1-bit "escape" flag
headerByte = (uint8_t) BitBufferRead( bits, 4 );
partialFrame = headerByte >> 3;
bytesShifted = (headerByte >> 1) & 0x3u;
RequireAction( bytesShifted != 3, status = kALAC_ParamError; goto Exit; );
shift = bytesShifted * 8;
escapeFlag = headerByte & 0x1;
chanBits = mConfig.bitDepth - (bytesShifted * 8) + 1;
// check for partial frame length to override requested numSamples
if ( partialFrame != 0 )
{
numSamples = BitBufferRead( bits, 16 ) << 16;
numSamples |= BitBufferRead( bits, 16 );
}
if ( escapeFlag == 0 )
{
// compressed frame, read rest of parameters
mixBits = (uint8_t) BitBufferRead( bits, 8 );
mixRes = (int8_t) BitBufferRead( bits, 8 );
headerByte = (uint8_t) BitBufferRead( bits, 8 );
modeU = headerByte >> 4;
denShiftU = headerByte & 0xfu;
headerByte = (uint8_t) BitBufferRead( bits, 8 );
pbFactorU = headerByte >> 5;
numU = headerByte & 0x1fu;
for ( i = 0; i < numU; i++ )
coefsU[i] = (int16_t) BitBufferRead( bits, 16 );
headerByte = (uint8_t) BitBufferRead( bits, 8 );
modeV = headerByte >> 4;
denShiftV = headerByte & 0xfu;
headerByte = (uint8_t) BitBufferRead( bits, 8 );
pbFactorV = headerByte >> 5;
numV = headerByte & 0x1fu;
for ( i = 0; i < numV; i++ )
coefsV[i] = (int16_t) BitBufferRead( bits, 16 );
// if shift active, skip the interleaved shifted values but remember where they start
if ( bytesShifted != 0 )
{
shiftBits = *bits;
BitBufferAdvance( bits, (bytesShifted * 8) * 2 * numSamples );
}
// decompress and run predictor for "left" channel
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorU) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits1 );
RequireNoErr( status, goto Exit; );
if ( modeU == 0 )
{
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
}
else
{
// the special "numActive == 31" mode can be done in-place
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
unpc_block( mPredictor, mMixBufferU, numSamples, &coefsU[0], numU, chanBits, denShiftU );
}
// decompress and run predictor for "right" channel
set_ag_params( &agParams, mConfig.mb, (pb * pbFactorV) / 4, mConfig.kb, numSamples, numSamples, mConfig.maxRun );
status = dyn_decomp( &agParams, bits, mPredictor, numSamples, chanBits, &bits2 );
RequireNoErr( status, goto Exit; );
if ( modeV == 0 )
{
unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV );
}
else
{
// the special "numActive == 31" mode can be done in-place
unpc_block( mPredictor, mPredictor, numSamples, nil, 31, chanBits, 0 );
unpc_block( mPredictor, mMixBufferV, numSamples, &coefsV[0], numV, chanBits, denShiftV );
}
}
else
{
//Assert( bytesShifted == 0 );
// uncompressed frame, copy data into the mix buffers to use common output code
chanBits = mConfig.bitDepth;
shift = 32 - chanBits;
if ( chanBits <= 16 )
{
for ( i = 0; i < numSamples; i++ )
{
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
val = (val << shift) >> shift;
mMixBufferU[i] = val;
val = (int32_t) BitBufferRead( bits, (uint8_t) chanBits );
val = (val << shift) >> shift;
mMixBufferV[i] = val;
}
}
else
{
// BitBufferRead() can't read more than 16 bits at a time so break up the reads
extraBits = chanBits - 16;
for ( i = 0; i < numSamples; i++ )
{
val = (int32_t) BitBufferRead( bits, 16 );
val = (val << 16) >> shift;
mMixBufferU[i] = val | BitBufferRead( bits, (uint8_t)extraBits );
val = (int32_t) BitBufferRead( bits, 16 );
val = (val << 16) >> shift;
mMixBufferV[i] = val | BitBufferRead( bits, (uint8_t)extraBits );
}
}
bits1 = chanBits * numSamples;
bits2 = chanBits * numSamples;
mixBits = mixRes = 0;
bytesShifted = 0;
}
// now read the shifted values into the shift buffer
if ( bytesShifted != 0 )
{
shift = bytesShifted * 8;
//Assert( shift <= 16 );
for ( i = 0; i < (numSamples * 2); i += 2 )
{
mShiftBuffer[i + 0] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
mShiftBuffer[i + 1] = (uint16_t) BitBufferRead( &shiftBits, (uint8_t) shift );
}
}
// un-mix the data and convert to output format
// - note that mixRes = 0 means just interleave so we use that path for uncompressed frames
switch ( mConfig.bitDepth )
{
case 16:
out16 = &((int16_t *)sampleBuffer)[channelIndex];
unmix16( mMixBufferU, mMixBufferV, out16, numChannels, numSamples, mixBits, mixRes );
break;
case 20:
out20 = (uint8_t *)sampleBuffer + (channelIndex * 3);
unmix20( mMixBufferU, mMixBufferV, out20, numChannels, numSamples, mixBits, mixRes );
break;
case 24:
out24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
unmix24( mMixBufferU, mMixBufferV, out24, numChannels, numSamples,
mixBits, mixRes, mShiftBuffer, bytesShifted );
break;
case 32:
out32 = &((int32_t *)sampleBuffer)[channelIndex];
unmix32( mMixBufferU, mMixBufferV, out32, numChannels, numSamples,
mixBits, mixRes, mShiftBuffer, bytesShifted );
break;
}
channelIndex += 2;
*outNumSamples = numSamples;
break;
}
case ID_CCE:
case ID_PCE:
{
// unsupported element, bail
//AssertNoErr( tag );
status = kALAC_ParamError;
break;
}
case ID_DSE:
{
// data stream element -- parse but ignore
status = this->DataStreamElement( bits );
break;
}
case ID_FIL:
{
// fill element -- parse but ignore
status = this->FillElement( bits );
break;
}
case ID_END:
{
// frame end, all done so byte align the frame and check for overruns
BitBufferByteAlign( bits, false );
//Assert( bits->cur == bits->end );
goto Exit;
}
}
#if ! DEBUG
// if we've decoded all of our channels, bail (but not in debug b/c we want to know if we're seeing bad bits)
// - this also protects us if the config does not match the bitstream or crap data bits follow the audio bits
if ( channelIndex >= numChannels )
break;
#endif
}
NoMoreChannels:
// if we get here and haven't decoded all of the requested channels, fill the remaining channels with zeros
for ( ; channelIndex < numChannels; channelIndex++ )
{
switch ( mConfig.bitDepth )
{
case 16:
{
int16_t * fill16 = &((int16_t *)sampleBuffer)[channelIndex];
Zero16( fill16, numSamples, numChannels );
break;
}
case 24:
{
uint8_t * fill24 = (uint8_t *)sampleBuffer + (channelIndex * 3);
Zero24( fill24, numSamples, numChannels );
break;
}
case 32:
{
int32_t * fill32 = &((int32_t *)sampleBuffer)[channelIndex];
Zero32( fill32, numSamples, numChannels );
break;
}
}
}
Exit:
return status;
}
#if PRAGMA_MARK
#pragma mark -
#endif
/*
FillElement()
- they're just filler so we don't need 'em
*/
int32_t ALACDecoder::FillElement( BitBuffer * bits )
{
int16_t count;
// 4-bit count or (4-bit + 8-bit count) if 4-bit count == 15
// - plus this weird -1 thing I still don't fully understand
count = BitBufferReadSmall( bits, 4 );
if ( count == 15 )
count += (int16_t) BitBufferReadSmall( bits, 8 ) - 1;
BitBufferAdvance( bits, count * 8 );
RequireAction( bits->cur <= bits->end, return kALAC_ParamError; );
return ALAC_noErr;
}
/*
DataStreamElement()
- we don't care about data stream elements so just skip them
*/
int32_t ALACDecoder::DataStreamElement( BitBuffer * bits )
{
uint8_t element_instance_tag;
int32_t data_byte_align_flag;
uint16_t count;
// the tag associates this data stream element with a given audio element
element_instance_tag = BitBufferReadSmall( bits, 4 );
data_byte_align_flag = BitBufferReadOne( bits );
// 8-bit count or (8-bit + 8-bit count) if 8-bit count == 255
count = BitBufferReadSmall( bits, 8 );
if ( count == 255 )
count += BitBufferReadSmall( bits, 8 );
// the align flag means the bitstream should be byte-aligned before reading the following data bytes
if ( data_byte_align_flag )
BitBufferByteAlign( bits, false );
// skip the data bytes
BitBufferAdvance( bits, count * 8 );
RequireAction( bits->cur <= bits->end, return kALAC_ParamError; );
return ALAC_noErr;
}
/*
ZeroN()
- helper routines to clear out output channel buffers when decoding fewer channels than requested
*/
static void Zero16( int16_t * buffer, uint32_t numItems, uint32_t stride )
{
if ( stride == 1 )
{
memset( buffer, 0, numItems * sizeof(int16_t) );
}
else
{
for ( uint32_t index = 0; index < (numItems * stride); index += stride )
buffer[index] = 0;
}
}
static void Zero24( uint8_t * buffer, uint32_t numItems, uint32_t stride )
{
if ( stride == 1 )
{
memset( buffer, 0, numItems * 3 );
}
else
{
for ( uint32_t index = 0; index < (numItems * stride * 3); index += (stride * 3) )
{
buffer[index + 0] = 0;
buffer[index + 1] = 0;
buffer[index + 2] = 0;
}
}
}
static void Zero32( int32_t * buffer, uint32_t numItems, uint32_t stride )
{
if ( stride == 1 )
{
memset( buffer, 0, numItems * sizeof(int32_t) );
}
else
{
for ( uint32_t index = 0; index < (numItems * stride); index += stride )
buffer[index] = 0;
}
}

1425
alac/codec/ALACEncoder.cpp Normal file

File diff suppressed because it is too large Load Diff

92
alac/codec/ALACEncoder.h Normal file
View File

@@ -0,0 +1,92 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: ALACEncoder.h
*/
#pragma once
#include <stdint.h>
#include "ALACAudioTypes.h"
struct BitBuffer;
class ALACEncoder
{
public:
ALACEncoder();
virtual ~ALACEncoder();
virtual int32_t Encode(AudioFormatDescription theInputFormat, AudioFormatDescription theOutputFormat,
unsigned char * theReadBuffer, unsigned char * theWriteBuffer, int32_t * ioNumBytes);
virtual int32_t Finish( );
void SetFastMode( bool fast ) { mFastMode = fast; };
// this must be called *before* InitializeEncoder()
void SetFrameSize( uint32_t frameSize ) { mFrameSize = frameSize; };
void GetConfig( ALACSpecificConfig & config );
uint32_t GetMagicCookieSize(uint32_t inNumChannels);
void GetMagicCookie( void * config, uint32_t * ioSize );
virtual int32_t InitializeEncoder(AudioFormatDescription theOutputFormat);
protected:
virtual void GetSourceFormat( const AudioFormatDescription * source, AudioFormatDescription * output );
int32_t EncodeStereo( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
int32_t EncodeStereoFast( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
int32_t EncodeStereoEscape( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t numSamples );
int32_t EncodeMono( struct BitBuffer * bitstream, void * input, uint32_t stride, uint32_t channelIndex, uint32_t numSamples );
// ALAC encoder parameters
int16_t mBitDepth;
bool mFastMode;
// encoding state
int16_t mLastMixRes[kALACMaxChannels];
// encoding buffers
int32_t * mMixBufferU;
int32_t * mMixBufferV;
int32_t * mPredictorU;
int32_t * mPredictorV;
uint16_t * mShiftBufferUV;
uint8_t * mWorkBuffer;
// per-channel coefficients buffers
int16_t mCoefsU[kALACMaxChannels][kALACMaxSearches][kALACMaxCoefs];
int16_t mCoefsV[kALACMaxChannels][kALACMaxSearches][kALACMaxCoefs];
// encoding statistics
uint32_t mTotalBytesGenerated;
uint32_t mAvgBitRate;
uint32_t mMaxFrameBytes;
uint32_t mFrameSize;
uint32_t mMaxOutputBytes;
uint32_t mNumChannels;
uint32_t mOutputSampleRate;
};

View File

@@ -0,0 +1,335 @@
APPLE PUBLIC SOURCE LICENSE
Version 2.0 - August 6, 2003
Please read this License carefully before downloading this software. By
downloading or using this software, you are agreeing to be bound by the terms
of this License. If you do not or cannot agree to the terms of this License,
please do not download or use the software.
Apple Note: In January 2007, Apple changed its corporate name from "Apple
Computer, Inc." to "Apple Inc." This change has been reflected below and
copyright years updated, but no other changes have been made to the APSL 2.0.
1. General; Definitions. This License applies to any program or other
work which Apple Inc. ("Apple") makes publicly available and which contains a
notice placed by Apple identifying such program or work as "Original Code" and
stating that it is subject to the terms of this Apple Public Source License
version 2.0 ("License"). As used in this License:
1.1 "Applicable Patent Rights" mean: (a) in the case where Apple is the
grantor of rights, (i) claims of patents that are now or hereafter acquired,
owned by or assigned to Apple and (ii) that cover subject matter contained in
the Original Code, but only to the extent necessary to use, reproduce and/or
distribute the Original Code without infringement; and (b) in the case where
You are the grantor of rights, (i) claims of patents that are now or hereafter
acquired, owned by or assigned to You and (ii) that cover subject matter in
Your Modifications, taken alone or in combination with Original Code.
1.2 "Contributor" means any person or entity that creates or contributes to
the creation of Modifications.
1.3 "Covered Code" means the Original Code, Modifications, the combination
of Original Code and any Modifications, and/or any respective portions thereof.
1.4 "Externally Deploy" means: (a) to sublicense, distribute or otherwise
make Covered Code available, directly or indirectly, to anyone other than You;
and/or (b) to use Covered Code, alone or as part of a Larger Work, in any way
to provide a service, including but not limited to delivery of content, through
electronic communication with a client other than You.
1.5 "Larger Work" means a work which combines Covered Code or portions
thereof with code not governed by the terms of this License.
1.6 "Modifications" mean any addition to, deletion from, and/or change to,
the substance and/or structure of the Original Code, any previous
Modifications, the combination of Original Code and any previous Modifications,
and/or any respective portions thereof. When code is released as a series of
files, a Modification is: (a) any addition to or deletion from the contents of
a file containing Covered Code; and/or (b) any new file or other representation
of computer program statements that contains any part of Covered Code.
1.7 "Original Code" means (a) the Source Code of a program or other work as
originally made available by Apple under this License, including the Source
Code of any updates or upgrades to such programs or works made available by
Apple under this License, and that has been expressly identified by Apple as
such in the header file(s) of such work; and (b) the object code compiled from
such Source Code and originally made available by Apple under this License
1.8 "Source Code" means the human readable form of a program or other work
that is suitable for making modifications to it, including all modules it
contains, plus any associated interface definition files, scripts used to
control compilation and installation of an executable (object code).
1.9 "You" or "Your" means an individual or a legal entity exercising rights
under this License. For legal entities, "You" or "Your" includes any entity
which controls, is controlled by, or is under common control with, You, where
"control" means (a) the power, direct or indirect, to cause the direction or
management of such entity, whether by contract or otherwise, or (b) ownership
of fifty percent (50%) or more of the outstanding shares or beneficial
ownership of such entity.
2. Permitted Uses; Conditions & Restrictions. Subject to the terms and
conditions of this License, Apple hereby grants You, effective on the date You
accept this License and download the Original Code, a world-wide, royalty-free,
non-exclusive license, to the extent of Apple's Applicable Patent Rights and
copyrights covering the Original Code, to do the following:
2.1 Unmodified Code. You may use, reproduce, display, perform, internally
distribute within Your organization, and Externally Deploy verbatim, unmodified
copies of the Original Code, for commercial or non-commercial purposes,
provided that in each instance:
(a) You must retain and reproduce in all copies of Original Code the
copyright and other proprietary notices and disclaimers of Apple as they appear
in the Original Code, and keep intact all notices in the Original Code that
refer to this License; and
(b) You must include a copy of this License with every copy of Source Code
of Covered Code and documentation You distribute or Externally Deploy, and You
may not offer or impose any terms on such Source Code that alter or restrict
this License or the recipients' rights hereunder, except as permitted under
Section 6.
2.2 Modified Code. You may modify Covered Code and use, reproduce,
display, perform, internally distribute within Your organization, and
Externally Deploy Your Modifications and Covered Code, for commercial or
non-commercial purposes, provided that in each instance You also meet all of
these conditions:
(a) You must satisfy all the conditions of Section 2.1 with respect to the
Source Code of the Covered Code;
(b) You must duplicate, to the extent it does not already exist, the notice
in Exhibit A in each file of the Source Code of all Your Modifications, and
cause the modified files to carry prominent notices stating that You changed
the files and the date of any change; and
(c) If You Externally Deploy Your Modifications, You must make Source Code
of all Your Externally Deployed Modifications either available to those to whom
You have Externally Deployed Your Modifications, or publicly available. Source
Code of Your Externally Deployed Modifications must be released under the terms
set forth in this License, including the license grants set forth in Section 3
below, for as long as you Externally Deploy the Covered Code or twelve (12)
months from the date of initial External Deployment, whichever is longer. You
should preferably distribute the Source Code of Your Externally Deployed
Modifications electronically (e.g. download from a web site).
2.3 Distribution of Executable Versions. In addition, if You Externally
Deploy Covered Code (Original Code and/or Modifications) in object code,
executable form only, You must include a prominent notice, in the code itself
as well as in related documentation, stating that Source Code of the Covered
Code is available under the terms of this License with information on how and
where to obtain such Source Code.
2.4 Third Party Rights. You expressly acknowledge and agree that although
Apple and each Contributor grants the licenses to their respective portions of
the Covered Code set forth herein, no assurances are provided by Apple or any
Contributor that the Covered Code does not infringe the patent or other
intellectual property rights of any other entity. Apple and each Contributor
disclaim any liability to You for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, You hereby assume sole
responsibility to secure any other intellectual property rights needed, if any.
For example, if a third party patent license is required to allow You to
distribute the Covered Code, it is Your responsibility to acquire that license
before distributing the Covered Code.
3. Your Grants. In consideration of, and as a condition to, the licenses
granted to You under this License, You hereby grant to any person or entity
receiving or distributing Covered Code under this License a non-exclusive,
royalty-free, perpetual, irrevocable license, under Your Applicable Patent
Rights and other intellectual property rights (other than patent) owned or
controlled by You, to use, reproduce, display, perform, modify, sublicense,
distribute and Externally Deploy Your Modifications of the same scope and
extent as Apple's licenses under Sections 2.1 and 2.2 above.
4. Larger Works. You may create a Larger Work by combining Covered Code
with other code not governed by the terms of this License and distribute the
Larger Work as a single product. In each such instance, You must make sure the
requirements of this License are fulfilled for the Covered Code or any portion
thereof.
5. Limitations on Patent License. Except as expressly stated in Section
2, no other patent rights, express or implied, are granted by Apple herein.
Modifications and/or Larger Works may require additional patent licenses from
Apple which Apple may grant in its sole discretion.
6. Additional Terms. You may choose to offer, and to charge a fee for,
warranty, support, indemnity or liability obligations and/or other rights
consistent with the scope of the license granted herein ("Additional Terms") to
one or more recipients of Covered Code. However, You may do so only on Your own
behalf and as Your sole responsibility, and not on behalf of Apple or any
Contributor. You must obtain the recipient's agreement that any such Additional
Terms are offered by You alone, and You hereby agree to indemnify, defend and
hold Apple and every Contributor harmless for any liability incurred by or
claims asserted against Apple or such Contributor by reason of any such
Additional Terms.
7. Versions of the License. Apple may publish revised and/or new versions
of this License from time to time. Each version will be given a distinguishing
version number. Once Original Code has been published under a particular
version of this License, You may continue to use it under the terms of that
version. You may also choose to use such Original Code under the terms of any
subsequent version of this License published by Apple. No one other than Apple
has the right to modify the terms applicable to Covered Code created under this
License.
8. NO WARRANTY OR SUPPORT. The Covered Code may contain in whole or in
part pre-release, untested, or not fully tested works. The Covered Code may
contain errors that could cause failures or loss of data, and may be incomplete
or contain inaccuracies. You expressly acknowledge and agree that use of the
Covered Code, or any portion thereof, is at Your sole and entire risk. THE
COVERED CODE IS PROVIDED "AS IS" AND WITHOUT WARRANTY, UPGRADES OR SUPPORT OF
ANY KIND AND APPLE AND APPLE'S LICENSOR(S) (COLLECTIVELY REFERRED TO AS "APPLE"
FOR THE PURPOSES OF SECTIONS 8 AND 9) AND ALL CONTRIBUTORS EXPRESSLY DISCLAIM
ALL WARRANTIES AND/OR CONDITIONS, EXPRESS OR IMPLIED, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF MERCHANTABILITY, OF
SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY, OF
QUIET ENJOYMENT, AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. APPLE AND EACH
CONTRIBUTOR DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE
COVERED CODE, THAT THE FUNCTIONS CONTAINED IN THE COVERED CODE WILL MEET YOUR
REQUIREMENTS, THAT THE OPERATION OF THE COVERED CODE WILL BE UNINTERRUPTED OR
ERROR-FREE, OR THAT DEFECTS IN THE COVERED CODE WILL BE CORRECTED. NO ORAL OR
WRITTEN INFORMATION OR ADVICE GIVEN BY APPLE, AN APPLE AUTHORIZED
REPRESENTATIVE OR ANY CONTRIBUTOR SHALL CREATE A WARRANTY. You acknowledge
that the Covered Code is not intended for use in the operation of nuclear
facilities, aircraft navigation, communication systems, or air traffic control
machines in which case the failure of the Covered Code could lead to death,
personal injury, or severe physical or environmental damage.
9. LIMITATION OF LIABILITY. TO THE EXTENT NOT PROHIBITED BY LAW, IN NO
EVENT SHALL APPLE OR ANY CONTRIBUTOR BE LIABLE FOR ANY INCIDENTAL, SPECIAL,
INDIRECT OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO THIS LICENSE OR
YOUR USE OR INABILITY TO USE THE COVERED CODE, OR ANY PORTION THEREOF, WHETHER
UNDER A THEORY OF CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCTS
LIABILITY OR OTHERWISE, EVEN IF APPLE OR SUCH CONTRIBUTOR HAS BEEN ADVISED OF
THE POSSIBILITY OF SUCH DAMAGES AND NOTWITHSTANDING THE FAILURE OF ESSENTIAL
PURPOSE OF ANY REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OF
LIABILITY OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT
APPLY TO YOU. In no event shall Apple's total liability to You for all damages
(other than as may be required by applicable law) under this License exceed the
amount of fifty dollars ($50.00).
10. Trademarks. This License does not grant any rights to use the
trademarks or trade names "Apple", "Mac", "Mac OS", "QuickTime", "QuickTime
Streaming Server" or any other trademarks, service marks, logos or trade names
belonging to Apple (collectively "Apple Marks") or to any trademark, service
mark, logo or trade name belonging to any Contributor. You agree not to use
any Apple Marks in or as part of the name of products derived from the Original
Code or to endorse or promote products derived from the Original Code other
than as expressly permitted by and in strict compliance at all times with
Apple's third party trademark usage guidelines which are posted at
http://www.apple.com/legal/guidelinesfor3rdparties.html.
11. Ownership. Subject to the licenses granted under this License, each
Contributor retains all rights, title and interest in and to any Modifications
made by such Contributor. Apple retains all rights, title and interest in and
to the Original Code and any Modifications made by or on behalf of Apple
("Apple Modifications"), and such Apple Modifications will not be automatically
subject to this License. Apple may, at its sole discretion, choose to license
such Apple Modifications under this License, or on different terms from those
contained in this License or may choose not to license them at all.
12. Termination.
12.1 Termination. This License and the rights granted hereunder will
terminate:
(a) automatically without notice from Apple if You fail to comply with any
term(s) of this License and fail to cure such breach within 30 days of becoming
aware of such breach;
(b) immediately in the event of the circumstances described in Section
13.5(b); or
(c) automatically without notice from Apple if You, at any time during the
term of this License, commence an action for patent infringement against Apple;
provided that Apple did not first commence an action for patent infringement
against You in that instance.
12.2 Effect of Termination. Upon termination, You agree to immediately stop
any further use, reproduction, modification, sublicensing and distribution of
the Covered Code. All sublicenses to the Covered Code which have been properly
granted prior to termination shall survive any termination of this License.
Provisions which, by their nature, should remain in effect beyond the
termination of this License shall survive, including but not limited to
Sections 3, 5, 8, 9, 10, 11, 12.2 and 13. No party will be liable to any other
for compensation, indemnity or damages of any sort solely as a result of
terminating this License in accordance with its terms, and termination of this
License will be without prejudice to any other right or remedy of any party.
13. Miscellaneous.
13.1 Government End Users. The Covered Code is a "commercial item" as
defined in FAR 2.101. Government software and technical data rights in the
Covered Code include only those rights customarily provided to the public as
defined in this License. This customary commercial license in technical data
and software is provided in accordance with FAR 12.211 (Technical Data) and
12.212 (Computer Software) and, for Department of Defense purchases, DFAR
252.227-7015 (Technical Data -- Commercial Items) and 227.7202-3 (Rights in
Commercial Computer Software or Computer Software Documentation). Accordingly,
all U.S. Government End Users acquire Covered Code with only those rights set
forth herein.
13.2 Relationship of Parties. This License will not be construed as
creating an agency, partnership, joint venture or any other form of legal
association between or among You, Apple or any Contributor, and You will not
represent to the contrary, whether expressly, by implication, appearance or
otherwise.
13.3 Independent Development. Nothing in this License will impair Apple's
right to acquire, license, develop, have others develop for it, market and/or
distribute technology or products that perform the same or similar functions
as, or otherwise compete with, Modifications, Larger Works, technology or
products that You may develop, produce, market or distribute.
13.4 Waiver; Construction. Failure by Apple or any Contributor to enforce
any provision of this License will not be deemed a waiver of future enforcement
of that or any other provision. Any law or regulation which provides that the
language of a contract shall be construed against the drafter will not apply to
this License.
13.5 Severability. (a) If for any reason a court of competent jurisdiction
finds any provision of this License, or portion thereof, to be unenforceable,
that provision of the License will be enforced to the maximum extent
permissible so as to effect the economic benefits and intent of the parties,
and the remainder of this License will continue in full force and effect. (b)
Notwithstanding the foregoing, if applicable law prohibits or restricts You
from fully and/or specifically complying with Sections 2 and/or 3 or prevents
the enforceability of either of those Sections, this License will immediately
terminate and You must immediately discontinue any use of the Covered Code and
destroy all copies of it that are in your possession or control.
13.6 Dispute Resolution. Any litigation or other dispute resolution between
You and Apple relating to this License shall take place in the Northern
District of California, and You and Apple hereby consent to the personal
jurisdiction of, and venue in, the state and federal courts within that
District with respect to this License. The application of the United Nations
Convention on Contracts for the International Sale of Goods is expressly
excluded.
13.7 Entire Agreement; Governing Law. This License constitutes the entire
agreement between the parties with respect to the subject matter hereof. This
License shall be governed by the laws of the United States and the State of
California, except that body of California law concerning conflicts of law.
Where You are located in the province of Quebec, Canada, the following clause
applies: The parties hereby confirm that they have requested that this License
and all related documents be drafted in English. Les parties ont exigé que le
présent contrat et tous les documents connexes soient rédigés en anglais.
EXHIBIT A.
"Portions Copyright (c) 1999-2007 Apple Inc. All Rights Reserved.
This file contains Original Code and/or Modifications of Original Code as
defined in and that are subject to the Apple Public Source License Version 2.0
(the 'License'). You may not use this file except in compliance with the
License. Please obtain a copy of the License at
http://www.opensource.apple.com/apsl/ and read it before using this file.
The Original Code and all software distributed under the License are
distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS
OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, INCLUDING WITHOUT
LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. Please see the License for the
specific language governing rights and limitations under the License."

362
alac/codec/ag_dec.c Normal file
View File

@@ -0,0 +1,362 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: ag_dec.c
Contains: Adaptive Golomb decode routines.
Copyright: (c) 2001-2011 Apple, Inc.
*/
#include "aglib.h"
#include "ALACBitUtilities.h"
#include "ALACAudioTypes.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if __GNUC__ && TARGET_OS_MAC
#if __POWERPC__
#include <ppc_intrinsics.h>
#else
#include <libkern/OSByteOrder.h>
#endif
#endif
#define CODE_TO_LONG_MAXBITS 32
#define N_MAX_MEAN_CLAMP 0xffff
#define N_MEAN_CLAMP_VAL 0xffff
#define REPORT_VAL 40
#if __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline))
#else
#define ALWAYS_INLINE
#endif
/* And on the subject of the CodeWarrior x86 compiler and inlining, I reworked a lot of this
to help the compiler out. In many cases this required manual inlining or a macro. Sorry
if it is ugly but the performance gains are well worth it.
- WSK 5/19/04
*/
void set_standard_ag_params(AGParamRecPtr params, uint32_t fullwidth, uint32_t sectorwidth)
{
/* Use
fullwidth = sectorwidth = numOfSamples, for analog 1-dimensional type-short data,
but use
fullwidth = full image width, sectorwidth = sector (patch) width
for such as image (2-dim.) data.
*/
set_ag_params( params, MB0, PB0, KB0, fullwidth, sectorwidth, MAX_RUN_DEFAULT );
}
void set_ag_params(AGParamRecPtr params, uint32_t m, uint32_t p, uint32_t k, uint32_t f, uint32_t s, uint32_t maxrun)
{
params->mb = params->mb0 = m;
params->pb = p;
params->kb = k;
params->wb = (1u<<params->kb)-1;
params->qb = QB-params->pb;
params->fw = f;
params->sw = s;
params->maxrun = maxrun;
}
#if PRAGMA_MARK
#pragma mark -
#endif
// note: implementing this with some kind of "count leading zeros" assembly is a big performance win
static inline int32_t lead( int32_t m )
{
long j;
unsigned long c = (1ul << 31);
for(j=0; j < 32; j++)
{
if((c & m) != 0)
break;
c >>= 1;
}
return (j);
}
#define arithmin(a, b) ((a) < (b) ? (a) : (b))
static inline int32_t ALWAYS_INLINE lg3a( int32_t x)
{
int32_t result;
x += 3;
result = lead(x);
return 31 - result;
}
static inline uint32_t ALWAYS_INLINE read32bit( uint8_t * buffer )
{
// embedded CPUs typically can't read unaligned 32-bit words so just read the bytes
uint32_t value;
value = ((uint32_t)buffer[0] << 24) | ((uint32_t)buffer[1] << 16) |
((uint32_t)buffer[2] << 8) | (uint32_t)buffer[3];
return value;
}
#if PRAGMA_MARK
#pragma mark -
#endif
#define get_next_fromlong(inlong, suff) ((inlong) >> (32 - (suff)))
static inline uint32_t ALWAYS_INLINE
getstreambits( uint8_t *in, int32_t bitoffset, int32_t numbits )
{
uint32_t load1, load2;
uint32_t byteoffset = bitoffset / 8;
uint32_t result;
//Assert( numbits <= 32 );
load1 = read32bit( in + byteoffset );
if ( (numbits + (bitoffset & 0x7)) > 32)
{
int32_t load2shift;
result = load1 << (bitoffset & 0x7);
load2 = (uint32_t) in[byteoffset+4];
load2shift = (8-(numbits + (bitoffset & 0x7)-32));
load2 >>= load2shift;
result >>= (32-numbits);
result |= load2;
}
else
{
result = load1 >> (32-numbits-(bitoffset & 7));
}
// a shift of >= "the number of bits in the type of the value being shifted" results in undefined
// behavior so don't try to shift by 32
if ( numbits != (sizeof(result) * 8) )
result &= ~(0xfffffffful << numbits);
return result;
}
static inline int32_t dyn_get(unsigned char *in, uint32_t *bitPos, uint32_t m, uint32_t k)
{
uint32_t tempbits = *bitPos;
uint32_t result;
uint32_t pre = 0, v;
uint32_t streamlong;
streamlong = read32bit( in + (tempbits >> 3) );
streamlong <<= (tempbits & 7);
/* find the number of bits in the prefix */
{
uint32_t notI = ~streamlong;
pre = lead( notI);
}
if(pre >= MAX_PREFIX_16)
{
pre = MAX_PREFIX_16;
tempbits += pre;
streamlong <<= pre;
result = get_next_fromlong(streamlong,MAX_DATATYPE_BITS_16);
tempbits += MAX_DATATYPE_BITS_16;
}
else
{
// all of the bits must fit within the long we have loaded
//Assert(pre+1+k <= 32);
tempbits += pre;
tempbits += 1;
streamlong <<= pre+1;
v = get_next_fromlong(streamlong, k);
tempbits += k;
result = pre*m + v-1;
if(v<2) {
result -= (v-1);
tempbits -= 1;
}
}
*bitPos = tempbits;
return result;
}
static inline int32_t dyn_get_32bit( uint8_t * in, uint32_t * bitPos, int32_t m, int32_t k, int32_t maxbits )
{
uint32_t tempbits = *bitPos;
uint32_t v;
uint32_t streamlong;
uint32_t result;
streamlong = read32bit( in + (tempbits >> 3) );
streamlong <<= (tempbits & 7);
/* find the number of bits in the prefix */
{
uint32_t notI = ~streamlong;
result = lead( notI);
}
if(result >= MAX_PREFIX_32)
{
result = getstreambits(in, tempbits+MAX_PREFIX_32, maxbits);
tempbits += MAX_PREFIX_32 + maxbits;
}
else
{
/* all of the bits must fit within the long we have loaded*/
//Assert(k<=14);
//Assert(result<MAX_PREFIX_32);
//Assert(result+1+k <= 32);
tempbits += result;
tempbits += 1;
if (k != 1)
{
streamlong <<= result+1;
v = get_next_fromlong(streamlong, k);
tempbits += k;
tempbits -= 1;
result = result*m;
if(v>=2)
{
result += (v-1);
tempbits += 1;
}
}
}
*bitPos = tempbits;
return result;
}
int32_t dyn_decomp( AGParamRecPtr params, BitBuffer * bitstream, int32_t * pc, int32_t numSamples, int32_t maxSize, uint32_t * outNumBits )
{
uint8_t *in;
int32_t *outPtr = pc;
uint32_t bitPos, startPos, maxPos;
uint32_t j, m, k, n, c, mz;
int32_t del, zmode;
uint32_t mb;
uint32_t pb_local = params->pb;
uint32_t kb_local = params->kb;
uint32_t wb_local = params->wb;
int32_t status;
RequireAction( (bitstream != nil) && (pc != nil) && (outNumBits != nil), return kALAC_ParamError; );
*outNumBits = 0;
in = bitstream->cur;
startPos = bitstream->bitIndex;
maxPos = bitstream->byteSize * 8;
bitPos = startPos;
mb = params->mb0;
zmode = 0;
c = 0;
status = ALAC_noErr;
while (c < numSamples)
{
// bail if we've run off the end of the buffer
RequireAction( bitPos < maxPos, status = kALAC_ParamError; goto Exit; );
m = (mb)>>QBSHIFT;
k = lg3a(m);
k = arithmin(k, kb_local);
m = (1<<k)-1;
n = dyn_get_32bit( in, &bitPos, m, k, maxSize );
// least significant bit is sign bit
{
uint32_t ndecode = n + zmode;
int32_t multiplier = (- (ndecode&1));
multiplier |= 1;
del = ((ndecode+1) >> 1) * (multiplier);
}
*outPtr++ = del;
c++;
mb = pb_local*(n+zmode) + mb - ((pb_local*mb)>>QBSHIFT);
// update mean tracking
if (n > N_MAX_MEAN_CLAMP)
mb = N_MEAN_CLAMP_VAL;
zmode = 0;
if (((mb << MMULSHIFT) < QB) && (c < numSamples))
{
zmode = 1;
k = lead(mb) - BITOFF+((mb+MOFF)>>MDENSHIFT);
mz = ((1<<k)-1) & wb_local;
n = dyn_get(in, &bitPos, mz, k);
RequireAction(c+n <= numSamples, status = kALAC_ParamError; goto Exit; );
for(j=0; j < n; j++)
{
*outPtr++ = 0;
++c;
}
if(n >= 65535)
zmode = 0;
mb = 0;
}
}
Exit:
*outNumBits = (bitPos - startPos);
BitBufferAdvance( bitstream, *outNumBits );
RequireAction( bitstream->cur <= bitstream->end, status = kALAC_ParamError; );
return status;
}

370
alac/codec/ag_enc.c Normal file
View File

@@ -0,0 +1,370 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: ag_enc.c
Contains: Adaptive Golomb encode routines.
Copyright: (c) 2001-2011 Apple, Inc.
*/
#include "aglib.h"
#include "ALACBitUtilities.h"
#include "EndianPortable.h"
#include "ALACAudioTypes.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if __GNUC__ && TARGET_OS_MAC
#if __POWERPC__
#include <ppc_intrinsics.h>
#else
#include <libkern/OSByteOrder.h>
#endif
#endif
#define CODE_TO_LONG_MAXBITS 32
#define N_MAX_MEAN_CLAMP 0xffff
#define N_MEAN_CLAMP_VAL 0xffff
#define REPORT_VAL 40
#if __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline))
#else
#define ALWAYS_INLINE
#endif
/* And on the subject of the CodeWarrior x86 compiler and inlining, I reworked a lot of this
to help the compiler out. In many cases this required manual inlining or a macro. Sorry
if it is ugly but the performance gains are well worth it.
- WSK 5/19/04
*/
// note: implementing this with some kind of "count leading zeros" assembly is a big performance win
static inline int32_t lead( int32_t m )
{
long j;
unsigned long c = (1ul << 31);
for(j=0; j < 32; j++)
{
if((c & m) != 0)
break;
c >>= 1;
}
return (j);
}
#define arithmin(a, b) ((a) < (b) ? (a) : (b))
static inline int32_t ALWAYS_INLINE lg3a( int32_t x)
{
int32_t result;
x += 3;
result = lead(x);
return 31 - result;
}
static inline int32_t ALWAYS_INLINE abs_func( int32_t a )
{
// note: the CW PPC intrinsic __abs() turns into these instructions so no need to try and use it
int32_t isneg = a >> 31;
int32_t xorval = a ^ isneg;
int32_t result = xorval-isneg;
return result;
}
static inline uint32_t ALWAYS_INLINE read32bit( uint8_t * buffer )
{
// embedded CPUs typically can't read unaligned 32-bit words so just read the bytes
uint32_t value;
value = ((uint32_t)buffer[0] << 24) | ((uint32_t)buffer[1] << 16) |
((uint32_t)buffer[2] << 8) | (uint32_t)buffer[3];
return value;
}
#if PRAGMA_MARK
#pragma mark -
#endif
static inline int32_t dyn_code(int32_t m, int32_t k, int32_t n, uint32_t *outNumBits)
{
uint32_t div, mod, de;
uint32_t numBits;
uint32_t value;
//Assert( n >= 0 );
div = n/m;
if(div >= MAX_PREFIX_16)
{
numBits = MAX_PREFIX_16 + MAX_DATATYPE_BITS_16;
value = (((1<<MAX_PREFIX_16)-1)<<MAX_DATATYPE_BITS_16) + n;
}
else
{
mod = n%m;
de = (mod == 0);
numBits = div + k + 1 - de;
value = (((1<<div)-1)<<(numBits-div)) + mod + 1 - de;
// if coding this way is bigger than doing escape, then do escape
if (numBits > MAX_PREFIX_16 + MAX_DATATYPE_BITS_16)
{
numBits = MAX_PREFIX_16 + MAX_DATATYPE_BITS_16;
value = (((1<<MAX_PREFIX_16)-1)<<MAX_DATATYPE_BITS_16) + n;
}
}
*outNumBits = numBits;
return (int32_t) value;
}
static inline int32_t dyn_code_32bit(int32_t maxbits, uint32_t m, uint32_t k, uint32_t n, uint32_t *outNumBits, uint32_t *outValue, uint32_t *overflow, uint32_t *overflowbits)
{
uint32_t div, mod, de;
uint32_t numBits;
uint32_t value;
int32_t didOverflow = 0;
div = n/m;
if (div < MAX_PREFIX_32)
{
mod = n - (m * div);
de = (mod == 0);
numBits = div + k + 1 - de;
value = (((1<<div)-1)<<(numBits-div)) + mod + 1 - de;
if (numBits > 25)
goto codeasescape;
}
else
{
codeasescape:
numBits = MAX_PREFIX_32;
value = (((1<<MAX_PREFIX_32)-1));
*overflow = n;
*overflowbits = maxbits;
didOverflow = 1;
}
*outNumBits = numBits;
*outValue = value;
return didOverflow;
}
static inline void ALWAYS_INLINE dyn_jam_noDeref(unsigned char *out, uint32_t bitPos, uint32_t numBits, uint32_t value)
{
uint32_t *i = (uint32_t *)(out + (bitPos >> 3));
uint32_t mask;
uint32_t curr;
uint32_t shift;
//Assert( numBits <= 32 );
curr = *i;
curr = Swap32NtoB( curr );
shift = 32 - (bitPos & 7) - numBits;
mask = ~0u >> (32 - numBits); // mask must be created in two steps to avoid compiler sequencing ambiguity
mask <<= shift;
value = (value << shift) & mask;
value |= curr & ~mask;
*i = Swap32BtoN( value );
}
static inline void ALWAYS_INLINE dyn_jam_noDeref_large(unsigned char *out, uint32_t bitPos, uint32_t numBits, uint32_t value)
{
uint32_t * i = (uint32_t *)(out + (bitPos>>3));
uint32_t w;
uint32_t curr;
uint32_t mask;
int32_t shiftvalue = (32 - (bitPos&7) - numBits);
//Assert(numBits <= 32);
curr = *i;
curr = Swap32NtoB( curr );
if (shiftvalue < 0)
{
uint8_t tailbyte;
uint8_t *tailptr;
w = value >> -shiftvalue;
mask = ~0u >> -shiftvalue;
w |= (curr & ~mask);
tailptr = ((uint8_t *)i) + 4;
tailbyte = (value << ((8+shiftvalue))) & 0xff;
*tailptr = (uint8_t)tailbyte;
}
else
{
mask = ~0u >> (32 - numBits);
mask <<= shiftvalue; // mask must be created in two steps to avoid compiler sequencing ambiguity
w = (value << shiftvalue) & mask;
w |= curr & ~mask;
}
*i = Swap32BtoN( w );
}
int32_t dyn_comp( AGParamRecPtr params, int32_t * pc, BitBuffer * bitstream, int32_t numSamples, int32_t bitSize, uint32_t * outNumBits )
{
unsigned char * out;
uint32_t bitPos, startPos;
uint32_t m, k, n, c, mz, nz;
uint32_t numBits;
uint32_t value;
int32_t del, zmode;
uint32_t overflow, overflowbits;
int32_t status;
// shadow the variables in params so there's not the dereferencing overhead
uint32_t mb, pb, kb, wb;
int32_t rowPos = 0;
int32_t rowSize = params->sw;
int32_t rowJump = (params->fw) - rowSize;
int32_t * inPtr = pc;
*outNumBits = 0;
RequireAction( (bitSize >= 1) && (bitSize <= 32), return kALAC_ParamError; );
out = bitstream->cur;
startPos = bitstream->bitIndex;
bitPos = startPos;
mb = params->mb = params->mb0;
pb = params->pb;
kb = params->kb;
wb = params->wb;
zmode = 0;
c=0;
status = ALAC_noErr;
while (c < numSamples)
{
m = mb >> QBSHIFT;
k = lg3a(m);
if ( k > kb)
{
k = kb;
}
m = (1<<k)-1;
del = *inPtr++;
rowPos++;
n = (abs_func(del) << 1) - ((del >> 31) & 1) - zmode;
//Assert( 32-lead(n) <= bitSize );
if ( dyn_code_32bit(bitSize, m, k, n, &numBits, &value, &overflow, &overflowbits) )
{
dyn_jam_noDeref(out, bitPos, numBits, value);
bitPos += numBits;
dyn_jam_noDeref_large(out, bitPos, overflowbits, overflow);
bitPos += overflowbits;
}
else
{
dyn_jam_noDeref(out, bitPos, numBits, value);
bitPos += numBits;
}
c++;
if ( rowPos >= rowSize)
{
rowPos = 0;
inPtr += rowJump;
}
mb = pb * (n + zmode) + mb - ((pb *mb)>>QBSHIFT);
// update mean tracking if it's overflowed
if (n > N_MAX_MEAN_CLAMP)
mb = N_MEAN_CLAMP_VAL;
zmode = 0;
RequireAction(c <= numSamples, status = kALAC_ParamError; goto Exit; );
if (((mb << MMULSHIFT) < QB) && (c < numSamples))
{
zmode = 1;
nz = 0;
while(c<numSamples && *inPtr == 0)
{
/* Take care of wrap-around globals. */
++inPtr;
++nz;
++c;
if ( ++rowPos >= rowSize)
{
rowPos = 0;
inPtr += rowJump;
}
if(nz >= 65535)
{
zmode = 0;
break;
}
}
k = lead(mb) - BITOFF+((mb+MOFF)>>MDENSHIFT);
mz = ((1<<k)-1) & wb;
value = dyn_code(mz, k, nz, &numBits);
dyn_jam_noDeref(out, bitPos, numBits, value);
bitPos += numBits;
mb = 0;
}
}
*outNumBits = (bitPos - startPos);
BitBufferAdvance( bitstream, *outNumBits );
Exit:
return status;
}

81
alac/codec/aglib.h Normal file
View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: aglib.h
Copyright: (C) 2001-2011 Apple, Inc.
*/
#ifndef AGLIB_H
#define AGLIB_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define QBSHIFT 9
#define QB (1<<QBSHIFT)
#define PB0 40
#define MB0 10
#define KB0 14
#define MAX_RUN_DEFAULT 255
#define MMULSHIFT 2
#define MDENSHIFT (QBSHIFT - MMULSHIFT - 1)
#define MOFF ((1<<(MDENSHIFT-2)))
#define BITOFF 24
/* Max. prefix of 1's. */
#define MAX_PREFIX_16 9
#define MAX_PREFIX_TOLONG_16 15
#define MAX_PREFIX_32 9
/* Max. bits in 16-bit data type */
#define MAX_DATATYPE_BITS_16 16
typedef struct AGParamRec
{
uint32_t mb, mb0, pb, kb, wb, qb;
uint32_t fw, sw;
uint32_t maxrun;
// fw = 1, sw = 1;
} AGParamRec, *AGParamRecPtr;
struct BitBuffer;
void set_standard_ag_params(AGParamRecPtr params, uint32_t fullwidth, uint32_t sectorwidth);
void set_ag_params(AGParamRecPtr params, uint32_t m, uint32_t p, uint32_t k, uint32_t f, uint32_t s, uint32_t maxrun);
int32_t dyn_comp(AGParamRecPtr params, int32_t * pc, struct BitBuffer * bitstream, int32_t numSamples, int32_t bitSize, uint32_t * outNumBits);
int32_t dyn_decomp(AGParamRecPtr params, struct BitBuffer * bitstream, int32_t * pc, int32_t numSamples, int32_t maxSize, uint32_t * outNumBits);
#ifdef __cplusplus
}
#endif
#endif //#ifndef AGLIB_H

386
alac/codec/dp_enc.c Normal file
View File

@@ -0,0 +1,386 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: dp_enc.c
Contains: Dynamic Predictor encode routines
Copyright: (c) 2001-2011 Apple, Inc.
*/
#include "dplib.h"
#include <string.h>
#if __GNUC__
#define ALWAYS_INLINE __attribute__((always_inline))
#else
#define ALWAYS_INLINE
#endif
#if TARGET_CPU_PPC && (__MWERKS__ >= 0x3200)
// align loops to a 16 byte boundary to make the G5 happy
#pragma function_align 16
#define LOOP_ALIGN asm { align 16 }
#else
#define LOOP_ALIGN
#endif
void init_coefs( int16_t * coefs, uint32_t denshift, int32_t numPairs )
{
int32_t k;
int32_t den = 1 << denshift;
coefs[0] = (AINIT * den) >> 4;
coefs[1] = (BINIT * den) >> 4;
coefs[2] = (CINIT * den) >> 4;
for ( k = 3; k < numPairs; k++ )
coefs[k] = 0;
}
void copy_coefs( int16_t * srcCoefs, int16_t * dstCoefs, int32_t numPairs )
{
int32_t k;
for ( k = 0; k < numPairs; k++ )
dstCoefs[k] = srcCoefs[k];
}
static inline int32_t ALWAYS_INLINE sign_of_int( int32_t i )
{
int32_t negishift;
negishift = ((uint32_t)-i) >> 31;
return negishift | (i >> 31);
}
void pc_block( int32_t * in, int32_t * pc1, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift )
{
register int16_t a0, a1, a2, a3;
register int32_t b0, b1, b2, b3;
int32_t j, k, lim;
int32_t * pin;
int32_t sum1, dd;
int32_t sg, sgn;
int32_t top;
int32_t del, del0;
uint32_t chanshift = 32 - chanbits;
int32_t denhalf = 1 << (denshift - 1);
pc1[0] = in[0];
if ( numactive == 0 )
{
// just copy if numactive == 0 (but don't bother if in/out pointers the same)
if ( (num > 1) && (in != pc1) )
memcpy( &pc1[1], &in[1], (num - 1) * sizeof(int32_t) );
return;
}
if ( numactive == 31 )
{
// short-circuit if numactive == 31
for( j = 1; j < num; j++ )
{
del = in[j] - in[j-1];
pc1[j] = (del << chanshift) >> chanshift;
}
return;
}
for ( j = 1; j <= numactive; j++ )
{
del = in[j] - in[j-1];
pc1[j] = (del << chanshift) >> chanshift;
}
lim = numactive + 1;
if ( numactive == 4 )
{
// optimization for numactive == 4
a0 = coefs[0];
a1 = coefs[1];
a2 = coefs[2];
a3 = coefs[3];
for ( j = lim; j < num; j++ )
{
LOOP_ALIGN
top = in[j - lim];
pin = in + j - 1;
b0 = top - pin[0];
b1 = top - pin[-1];
b2 = top - pin[-2];
b3 = top - pin[-3];
sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3) >> denshift;
del = in[j] - top - sum1;
del = (del << chanshift) >> chanshift;
pc1[j] = del;
del0 = del;
sg = sign_of_int(del);
if ( sg > 0 )
{
sgn = sign_of_int( b3 );
a3 -= sgn;
del0 -= (4 - 3) * ((sgn * b3) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b2 );
a2 -= sgn;
del0 -= (4 - 2) * ((sgn * b2) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b1 );
a1 -= sgn;
del0 -= (4 - 1) * ((sgn * b1) >> denshift);
if ( del0 <= 0 )
continue;
a0 -= sign_of_int( b0 );
}
else if ( sg < 0 )
{
// note: to avoid unnecessary negations, we flip the value of "sgn"
sgn = -sign_of_int( b3 );
a3 -= sgn;
del0 -= (4 - 3) * ((sgn * b3) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b2 );
a2 -= sgn;
del0 -= (4 - 2) * ((sgn * b2) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b1 );
a1 -= sgn;
del0 -= (4 - 1) * ((sgn * b1) >> denshift);
if ( del0 >= 0 )
continue;
a0 += sign_of_int( b0 );
}
}
coefs[0] = a0;
coefs[1] = a1;
coefs[2] = a2;
coefs[3] = a3;
}
else if ( numactive == 8 )
{
// optimization for numactive == 8
register int16_t a4, a5, a6, a7;
register int32_t b4, b5, b6, b7;
a0 = coefs[0];
a1 = coefs[1];
a2 = coefs[2];
a3 = coefs[3];
a4 = coefs[4];
a5 = coefs[5];
a6 = coefs[6];
a7 = coefs[7];
for ( j = lim; j < num; j++ )
{
LOOP_ALIGN
top = in[j - lim];
pin = in + j - 1;
b0 = top - (*pin--);
b1 = top - (*pin--);
b2 = top - (*pin--);
b3 = top - (*pin--);
b4 = top - (*pin--);
b5 = top - (*pin--);
b6 = top - (*pin--);
b7 = top - (*pin);
pin += 8;
sum1 = (denhalf - a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3
- a4 * b4 - a5 * b5 - a6 * b6 - a7 * b7) >> denshift;
del = in[j] - top - sum1;
del = (del << chanshift) >> chanshift;
pc1[j] = del;
del0 = del;
sg = sign_of_int(del);
if ( sg > 0 )
{
sgn = sign_of_int( b7 );
a7 -= sgn;
del0 -= 1 * ((sgn * b7) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b6 );
a6 -= sgn;
del0 -= 2 * ((sgn * b6) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b5 );
a5 -= sgn;
del0 -= 3 * ((sgn * b5) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b4 );
a4 -= sgn;
del0 -= 4 * ((sgn * b4) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b3 );
a3 -= sgn;
del0 -= 5 * ((sgn * b3) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b2 );
a2 -= sgn;
del0 -= 6 * ((sgn * b2) >> denshift);
if ( del0 <= 0 )
continue;
sgn = sign_of_int( b1 );
a1 -= sgn;
del0 -= 7 * ((sgn * b1) >> denshift);
if ( del0 <= 0 )
continue;
a0 -= sign_of_int( b0 );
}
else if ( sg < 0 )
{
// note: to avoid unnecessary negations, we flip the value of "sgn"
sgn = -sign_of_int( b7 );
a7 -= sgn;
del0 -= 1 * ((sgn * b7) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b6 );
a6 -= sgn;
del0 -= 2 * ((sgn * b6) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b5 );
a5 -= sgn;
del0 -= 3 * ((sgn * b5) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b4 );
a4 -= sgn;
del0 -= 4 * ((sgn * b4) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b3 );
a3 -= sgn;
del0 -= 5 * ((sgn * b3) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b2 );
a2 -= sgn;
del0 -= 6 * ((sgn * b2) >> denshift);
if ( del0 >= 0 )
continue;
sgn = -sign_of_int( b1 );
a1 -= sgn;
del0 -= 7 * ((sgn * b1) >> denshift);
if ( del0 >= 0 )
continue;
a0 += sign_of_int( b0 );
}
}
coefs[0] = a0;
coefs[1] = a1;
coefs[2] = a2;
coefs[3] = a3;
coefs[4] = a4;
coefs[5] = a5;
coefs[6] = a6;
coefs[7] = a7;
}
else
{
//pc_block_general:
// general case
for ( j = lim; j < num; j++ )
{
LOOP_ALIGN
top = in[j - lim];
pin = in + j - 1;
sum1 = 0;
for ( k = 0; k < numactive; k++ )
sum1 -= coefs[k] * (top - pin[-k]);
del = in[j] - top - ((sum1 + denhalf) >> denshift);
del = (del << chanshift) >> chanshift;
pc1[j] = del;
del0 = del;
sg = sign_of_int( del );
if ( sg > 0 )
{
for ( k = (numactive - 1); k >= 0; k-- )
{
dd = top - pin[-k];
sgn = sign_of_int( dd );
coefs[k] -= sgn;
del0 -= (numactive - k) * ((sgn * dd) >> denshift);
if ( del0 <= 0 )
break;
}
}
else if ( sg < 0 )
{
for ( k = (numactive - 1); k >= 0; k-- )
{
dd = top - pin[-k];
sgn = sign_of_int( dd );
coefs[k] += sgn;
del0 -= (numactive - k) * ((-sgn * dd) >> denshift);
if ( del0 >= 0 )
break;
}
}
}
}
}

61
alac/codec/dplib.h Normal file
View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: dplib.h
Contains: Dynamic Predictor routines
Copyright: Copyright (C) 2001-2011 Apple, Inc.
*/
#ifndef __DPLIB_H__
#define __DPLIB_H__
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// defines
#define DENSHIFT_MAX 15
#define DENSHIFT_DEFAULT 9
#define AINIT 38
#define BINIT (-29)
#define CINIT (-2)
#define NUMCOEPAIRS 16
// prototypes
void init_coefs( int16_t * coefs, uint32_t denshift, int32_t numPairs );
void copy_coefs( int16_t * srcCoefs, int16_t * dstCoefs, int32_t numPairs );
// NOTE: these routines read at least "numactive" samples so the i/o buffers must be at least that big
void pc_block( int32_t * in, int32_t * pc, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift );
void unpc_block( int32_t * pc, int32_t * out, int32_t num, int16_t * coefs, int32_t numactive, uint32_t chanbits, uint32_t denshift );
#ifdef __cplusplus
}
#endif
#endif /* __DPLIB_H__ */

80
alac/codec/makefile Normal file
View File

@@ -0,0 +1,80 @@
# libalac make
CFLAGS = -g -O3 -c
LFLAGS = -Wall
CC = g++
SRCDIR = .
OBJDIR = ./obj
INCLUDES = .
HEADERS = \
$(SRCDIR)/EndianPortable.h \
$(SRCDIR)/aglib.h \
$(SRCDIR)/ALACAudioTypes.h \
$(SRCDIR)/ALACBitUtilities.h\
$(SRCDIR)/ALACDecoder.h \
$(SRCDIR)/ALACEncoder.h \
$(SRCDIR)/dplib.h \
$(SRCDIR)/matrixlib.h
SOURCES = \
$(SRCDIR)/EndianPortable.c \
$(SRCDIR)/ALACBitUtilities.c \
$(SRCDIR)/ALACDecoder.cpp \
$(SRCDIR)/ALACEncoder.cpp \
$(SRCDIR)/ag_dec.c \
$(SRCDIR)/ag_enc.c \
$(SRCDIR)/dp_dec.c \
$(SRCDIR)/dp_enc.c \
$(SRCDIR)/matrix_dec.c \
$(SRCDIR)/matrix_enc.c
OBJS = \
EndianPortable.o \
ALACBitUtilities.o \
ALACDecoder.o \
ALACEncoder.o \
ag_dec.o \
ag_enc.o \
dp_dec.o \
dp_enc.o \
matrix_dec.o \
matrix_enc.o
libalac.a: $(OBJS)
ar rcs libalac.a $(OBJS)
EndianPortable.o : EndianPortable.c
$(CC) -I $(INCLUDES) $(CFLAGS) EndianPortable.c
ALACBitUtilities.o : ALACBitUtilities.c
$(CC) -I $(INCLUDES) $(CFLAGS) ALACBitUtilities.c
ALACDecoder.o : ALACDecoder.cpp
$(CC) -I $(INCLUDES) $(CFLAGS) ALACDecoder.cpp
ALACEncoder.o : ALACEncoder.cpp
$(CC) -I $(INCLUDES) $(CFLAGS) ALACEncoder.cpp
ag_dec.o : ag_dec.c
$(CC) -I $(INCLUDES) $(CFLAGS) ag_dec.c
ag_enc.o : ag_enc.c
$(CC) -I $(INCLUDES) $(CFLAGS) ag_enc.c
dp_dec.o : dp_dec.c
$(CC) -I $(INCLUDES) $(CFLAGS) dp_dec.c
dp_enc.o : dp_enc.c
$(CC) -I $(INCLUDES) $(CFLAGS) dp_enc.c
matrix_dec.o : matrix_dec.c
$(CC) -I $(INCLUDES) $(CFLAGS) matrix_dec.c
matrix_enc.o : matrix_enc.c
$(CC) -I $(INCLUDES) $(CFLAGS) matrix_enc.c
clean:
-rm $(OBJS) libalac.a

390
alac/codec/matrix_dec.c Normal file
View File

@@ -0,0 +1,390 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: matrix_dec.c
Contains: ALAC mixing/matrixing decode routines.
Copyright: (c) 2004-2011 Apple, Inc.
*/
#include "matrixlib.h"
#include "ALACAudioTypes.h"
// up to 24-bit "offset" macros for the individual bytes of a 20/24-bit word
#if TARGET_RT_BIG_ENDIAN
#define LBYTE 2
#define MBYTE 1
#define HBYTE 0
#else
#define LBYTE 0
#define MBYTE 1
#define HBYTE 2
#endif
/*
There is no plain middle-side option; instead there are various mixing
modes including middle-side, each lossless, as embodied in the mix()
and unmix() functions. These functions exploit a generalized middle-side
transformation:
u := [(rL + (m-r)R)/m];
v := L - R;
where [ ] denotes integer floor. The (lossless) inverse is
L = u + v - [rV/m];
R = L - v;
*/
// 16-bit routines
void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres )
{
int16_t * op = out;
int32_t j;
if ( mixres != 0 )
{
/* matrixed stereo */
for ( j = 0; j < numSamples; j++ )
{
int32_t l, r;
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
r = l - v[j];
op[0] = (int16_t) l;
op[1] = (int16_t) r;
op += stride;
}
}
else
{
/* Conventional separated stereo. */
for ( j = 0; j < numSamples; j++ )
{
op[0] = (int16_t) u[j];
op[1] = (int16_t) v[j];
op += stride;
}
}
}
// 20-bit routines
// - the 20 bits of data are left-justified in 3 bytes of storage but right-aligned for input/output predictor buffers
void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres )
{
uint8_t * op = out;
int32_t j;
if ( mixres != 0 )
{
/* matrixed stereo */
for ( j = 0; j < numSamples; j++ )
{
int32_t l, r;
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
r = l - v[j];
l <<= 4;
r <<= 4;
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
op += 3;
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
else
{
/* Conventional separated stereo. */
for ( j = 0; j < numSamples; j++ )
{
int32_t val;
val = u[j] << 4;
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += 3;
val = v[j] << 4;
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
}
// 24-bit routines
// - the 24 bits of data are right-justified in the input/output predictor buffers
void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
{
uint8_t * op = out;
int32_t shift = bytesShifted * 8;
int32_t l, r;
int32_t j, k;
if ( mixres != 0 )
{
/* matrixed stereo */
if ( bytesShifted != 0 )
{
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
r = l - v[j];
l = (l << shift) | (uint32_t) shiftUV[k + 0];
r = (r << shift) | (uint32_t) shiftUV[k + 1];
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
op += 3;
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
else
{
for ( j = 0; j < numSamples; j++ )
{
l = u[j] + v[j] - ((mixres * v[j]) >> mixbits);
r = l - v[j];
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
op += 3;
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
}
else
{
/* Conventional separated stereo. */
if ( bytesShifted != 0 )
{
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = u[j];
r = v[j];
l = (l << shift) | (uint32_t) shiftUV[k + 0];
r = (r << shift) | (uint32_t) shiftUV[k + 1];
op[HBYTE] = (uint8_t)((l >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((l >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((l >> 0) & 0xffu);
op += 3;
op[HBYTE] = (uint8_t)((r >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((r >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((r >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
else
{
for ( j = 0; j < numSamples; j++ )
{
int32_t val;
val = u[j];
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += 3;
val = v[j];
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += (stride - 1) * 3;
}
}
}
}
// 32-bit routines
// - note that these really expect the internal data width to be < 32 but the arrays are 32-bit
// - otherwise, the calculations might overflow into the 33rd bit and be lost
// - therefore, these routines deal with the specified "unused lower" bytes in the "shift" buffers
void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
{
int32_t * op = out;
int32_t shift = bytesShifted * 8;
int32_t l, r;
int32_t j, k;
if ( mixres != 0 )
{
//Assert( bytesShifted != 0 );
/* matrixed stereo with shift */
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
int32_t lt, rt;
lt = u[j];
rt = v[j];
l = lt + rt - ((mixres * rt) >> mixbits);
r = l - rt;
op[0] = (l << shift) | (uint32_t) shiftUV[k + 0];
op[1] = (r << shift) | (uint32_t) shiftUV[k + 1];
op += stride;
}
}
else
{
if ( bytesShifted == 0 )
{
/* interleaving w/o shift */
for ( j = 0; j < numSamples; j++ )
{
op[0] = u[j];
op[1] = v[j];
op += stride;
}
}
else
{
/* interleaving with shift */
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
op[0] = (u[j] << shift) | (uint32_t) shiftUV[k + 0];
op[1] = (v[j] << shift) | (uint32_t) shiftUV[k + 1];
op += stride;
}
}
}
}
// 20/24-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples )
{
uint8_t * op = out;
int32_t j;
for ( j = 0; j < numSamples; j++ )
{
int32_t val = in[j];
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += (stride * 3);
}
}
void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted )
{
uint8_t * op = out;
int32_t shiftVal = bytesShifted * 8;
int32_t j;
//Assert( bytesShifted != 0 );
for ( j = 0; j < numSamples; j++ )
{
int32_t val = in[j];
val = (val << shiftVal) | (uint32_t) shift[j];
op[HBYTE] = (uint8_t)((val >> 16) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 8) & 0xffu);
op[LBYTE] = (uint8_t)((val >> 0) & 0xffu);
op += (stride * 3);
}
}
void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples )
{
uint8_t * op = out;
int32_t j;
// 32-bit predictor values are right-aligned but 20-bit output values should be left-aligned
// in the 24-bit output buffer
for ( j = 0; j < numSamples; j++ )
{
int32_t val = in[j];
op[HBYTE] = (uint8_t)((val >> 12) & 0xffu);
op[MBYTE] = (uint8_t)((val >> 4) & 0xffu);
op[LBYTE] = (uint8_t)((val << 4) & 0xffu);
op += (stride * 3);
}
}
void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples )
{
int32_t i, j;
// this is only a subroutine to abstract the "iPod can only output 16-bit data" problem
for ( i = 0, j = 0; i < numSamples; i++, j += stride )
out[j] = in[i];
}
void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted )
{
int32_t * op = out;
uint32_t shiftVal = bytesShifted * 8;
int32_t j;
//Assert( bytesShifted != 0 );
// this is only a subroutine to abstract the "iPod can only output 16-bit data" problem
for ( j = 0; j < numSamples; j++ )
{
op[0] = (in[j] << shiftVal) | (uint32_t) shift[j];
op += stride;
}
}

342
alac/codec/matrix_enc.c Normal file
View File

@@ -0,0 +1,342 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: matrix_enc.c
Contains: ALAC mixing/matrixing encode routines.
Copyright: (c) 2004-2011 Apple, Inc.
*/
#include "matrixlib.h"
#include "ALACAudioTypes.h"
// up to 24-bit "offset" macros for the individual bytes of a 20/24-bit word
#if TARGET_RT_BIG_ENDIAN
#define LBYTE 2
#define MBYTE 1
#define HBYTE 0
#else
#define LBYTE 0
#define MBYTE 1
#define HBYTE 2
#endif
/*
There is no plain middle-side option; instead there are various mixing
modes including middle-side, each lossless, as embodied in the mix()
and unmix() functions. These functions exploit a generalized middle-side
transformation:
u := [(rL + (m-r)R)/m];
v := L - R;
where [ ] denotes integer floor. The (lossless) inverse is
L = u + v - [rV/m];
R = L - v;
*/
// 16-bit routines
void mix16( int16_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres )
{
int16_t * ip = in;
int32_t j;
if ( mixres != 0 )
{
int32_t mod = 1 << mixbits;
int32_t m2;
/* matrixed stereo */
m2 = mod - mixres;
for ( j = 0; j < numSamples; j++ )
{
int32_t l, r;
l = (int32_t) ip[0];
r = (int32_t) ip[1];
ip += stride;
u[j] = (mixres * l + m2 * r) >> mixbits;
v[j] = l - r;
}
}
else
{
/* Conventional separated stereo. */
for ( j = 0; j < numSamples; j++ )
{
u[j] = (int32_t) ip[0];
v[j] = (int32_t) ip[1];
ip += stride;
}
}
}
// 20-bit routines
// - the 20 bits of data are left-justified in 3 bytes of storage but right-aligned for input/output predictor buffers
void mix20( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres )
{
int32_t l, r;
uint8_t * ip = in;
int32_t j;
if ( mixres != 0 )
{
/* matrixed stereo */
int32_t mod = 1 << mixbits;
int32_t m2 = mod - mixres;
for ( j = 0; j < numSamples; j++ )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
l = (l << 8) >> 12;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
r = (r << 8) >> 12;
ip += (stride - 1) * 3;
u[j] = (mixres * l + m2 * r) >> mixbits;
v[j] = l - r;
}
}
else
{
/* Conventional separated stereo. */
for ( j = 0; j < numSamples; j++ )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
u[j] = (l << 8) >> 12;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
v[j] = (r << 8) >> 12;
ip += (stride - 1) * 3;
}
}
}
// 24-bit routines
// - the 24 bits of data are right-justified in the input/output predictor buffers
void mix24( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
{
int32_t l, r;
uint8_t * ip = in;
int32_t shift = bytesShifted * 8;
uint32_t mask = (1ul << shift) - 1;
int32_t j, k;
if ( mixres != 0 )
{
/* matrixed stereo */
int32_t mod = 1 << mixbits;
int32_t m2 = mod - mixres;
if ( bytesShifted != 0 )
{
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
l = (l << 8) >> 8;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
r = (r << 8) >> 8;
ip += (stride - 1) * 3;
shiftUV[k + 0] = (uint16_t)(l & mask);
shiftUV[k + 1] = (uint16_t)(r & mask);
l >>= shift;
r >>= shift;
u[j] = (mixres * l + m2 * r) >> mixbits;
v[j] = l - r;
}
}
else
{
for ( j = 0; j < numSamples; j++ )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
l = (l << 8) >> 8;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
r = (r << 8) >> 8;
ip += (stride - 1) * 3;
u[j] = (mixres * l + m2 * r) >> mixbits;
v[j] = l - r;
}
}
}
else
{
/* Conventional separated stereo. */
if ( bytesShifted != 0 )
{
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
l = (l << 8) >> 8;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
r = (r << 8) >> 8;
ip += (stride - 1) * 3;
shiftUV[k + 0] = (uint16_t)(l & mask);
shiftUV[k + 1] = (uint16_t)(r & mask);
l >>= shift;
r >>= shift;
u[j] = l;
v[j] = r;
}
}
else
{
for ( j = 0; j < numSamples; j++ )
{
l = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
u[j] = (l << 8) >> 8;
ip += 3;
r = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
v[j] = (r << 8) >> 8;
ip += (stride - 1) * 3;
}
}
}
}
// 32-bit routines
// - note that these really expect the internal data width to be < 32 but the arrays are 32-bit
// - otherwise, the calculations might overflow into the 33rd bit and be lost
// - therefore, these routines deal with the specified "unused lower" bytes in the "shift" buffers
void mix32( int32_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted )
{
int32_t * ip = in;
int32_t shift = bytesShifted * 8;
uint32_t mask = (1ul << shift) - 1;
int32_t l, r;
int32_t j, k;
if ( mixres != 0 )
{
int32_t mod = 1 << mixbits;
int32_t m2;
//Assert( bytesShifted != 0 );
/* matrixed stereo with shift */
m2 = mod - mixres;
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = ip[0];
r = ip[1];
ip += stride;
shiftUV[k + 0] = (uint16_t)(l & mask);
shiftUV[k + 1] = (uint16_t)(r & mask);
l >>= shift;
r >>= shift;
u[j] = (mixres * l + m2 * r) >> mixbits;
v[j] = l - r;
}
}
else
{
if ( bytesShifted == 0 )
{
/* de-interleaving w/o shift */
for ( j = 0; j < numSamples; j++ )
{
u[j] = ip[0];
v[j] = ip[1];
ip += stride;
}
}
else
{
/* de-interleaving with shift */
for ( j = 0, k = 0; j < numSamples; j++, k += 2 )
{
l = ip[0];
r = ip[1];
ip += stride;
shiftUV[k + 0] = (uint16_t)(l & mask);
shiftUV[k + 1] = (uint16_t)(r & mask);
l >>= shift;
r >>= shift;
u[j] = l;
v[j] = r;
}
}
}
}
// 20/24-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
void copy20ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples )
{
uint8_t * ip = in;
int32_t j;
for ( j = 0; j < numSamples; j++ )
{
int32_t val;
// 20-bit values are left-aligned in the 24-bit input buffer but right-aligned in the 32-bit output buffer
val = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
out[j] = (val << 8) >> 12;
ip += stride * 3;
}
}
void copy24ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples )
{
uint8_t * ip = in;
int32_t j;
for ( j = 0; j < numSamples; j++ )
{
int32_t val;
val = (int32_t)( ((uint32_t)ip[HBYTE] << 16) | ((uint32_t)ip[MBYTE] << 8) | (uint32_t)ip[LBYTE] );
out[j] = (val << 8) >> 8;
ip += stride * 3;
}
}

80
alac/codec/matrixlib.h Normal file
View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2011 Apple Inc. All rights reserved.
*
* @APPLE_APACHE_LICENSE_HEADER_START@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @APPLE_APACHE_LICENSE_HEADER_END@
*/
/*
File: matrixlib.h
Contains: ALAC mixing/matrixing routines to/from 32-bit predictor buffers.
Copyright: Copyright (C) 2004 to 2011 Apple, Inc.
*/
#ifndef __MATRIXLIB_H
#define __MATRIXLIB_H
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
// 16-bit routines
void mix16( int16_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres );
void unmix16( int32_t * u, int32_t * v, int16_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres );
// 20-bit routines
void mix20( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples, int32_t mixbits, int32_t mixres );
void unmix20( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t mixbits, int32_t mixres );
// 24-bit routines
// - 24-bit data sometimes compresses better by shifting off the bottom byte so these routines deal with
// the specified "unused lower bytes" in the combined "shift" buffer
void mix24( uint8_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
void unmix24( int32_t * u, int32_t * v, uint8_t * out, uint32_t stride, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
// 32-bit routines
// - note that these really expect the internal data width to be < 32-bit but the arrays are 32-bit
// - otherwise, the calculations might overflow into the 33rd bit and be lost
// - therefore, these routines deal with the specified "unused lower" bytes in the combined "shift" buffer
void mix32( int32_t * in, uint32_t stride, int32_t * u, int32_t * v, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
void unmix32( int32_t * u, int32_t * v, int32_t * out, uint32_t stride, int32_t numSamples,
int32_t mixbits, int32_t mixres, uint16_t * shiftUV, int32_t bytesShifted );
// 20/24/32-bit <-> 32-bit helper routines (not really matrixing but convenient to put here)
void copy20ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples );
void copy24ToPredictor( uint8_t * in, uint32_t stride, int32_t * out, int32_t numSamples );
void copyPredictorTo24( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples );
void copyPredictorTo24Shift( int32_t * in, uint16_t * shift, uint8_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted );
void copyPredictorTo20( int32_t * in, uint8_t * out, uint32_t stride, int32_t numSamples );
void copyPredictorTo32( int32_t * in, int32_t * out, uint32_t stride, int32_t numSamples );
void copyPredictorTo32Shift( int32_t * in, uint16_t * shift, int32_t * out, uint32_t stride, int32_t numSamples, int32_t bytesShifted );
#ifdef __cplusplus
}
#endif
#endif /* __MATRIXLIB_H */

1
alac/readme.md Normal file
View File

@@ -0,0 +1 @@
apple lossless audio

View File

@@ -0,0 +1,16 @@
using System;
namespace InnerTube
{
public class CacheItem<T>
{
public T Item;
public DateTimeOffset ExpireTime;
public CacheItem(T item, TimeSpan expiresIn)
{
Item = item;
ExpireTime = DateTimeOffset.Now.Add(expiresIn);
}
}
}

12
core/InnerTube/Enums.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace InnerTube
{
public enum ChannelTabs
{
Home,
Videos,
Playlists,
Community,
Channels,
About
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,380 @@
using System;
using System.Xml;
using System.Xml.Linq;
namespace InnerTube.Models
{
public class DynamicItem
{
public string Id;
public string Title;
public Thumbnail[] Thumbnails;
public virtual XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("DynamicItem");
item.SetAttribute("id", Id);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class VideoItem : DynamicItem
{
public string UploadedAt;
public long Views;
public Channel Channel;
public string Duration;
public string Description;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("duration", Duration);
item.SetAttribute("views", Views.ToString());
item.SetAttribute("uploadedAt", UploadedAt);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
if (Channel is not null)
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
if (!string.IsNullOrWhiteSpace(Description))
{
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
}
return item;
}
}
public class PlaylistItem : DynamicItem
{
public int VideoCount;
public string FirstVideoId;
public Channel Channel;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Playlist");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class RadioItem : DynamicItem
{
public string FirstVideoId;
public Channel Channel;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Radio");
item.SetAttribute("id", Id);
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ChannelItem : DynamicItem
{
public string Url;
public string Description;
public long VideoCount;
public string Subscribers;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Channel");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("subscribers", Subscribers);
if (!string.IsNullOrWhiteSpace(Url))
item.SetAttribute("customUrl", Url);
XmlElement title = doc.CreateElement("Name");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Avatar");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ContinuationItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Continuation");
item.SetAttribute("key", Id);
return item;
}
}
public class ShelfItem : DynamicItem
{
public DynamicItem[] Items;
public int CollapsedItemCount;
public BadgeItem[] Badges;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Shelf");
item.SetAttribute("title", Title);
item.SetAttribute("collapsedItemCount", CollapsedItemCount.ToString());
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
if (Badges.Length > 0)
{
XmlElement badges = doc.CreateElement("Badges");
foreach (BadgeItem badge in Badges) badges.AppendChild(badge.GetXmlElement(doc));
item.AppendChild(badges);
}
XmlElement items = doc.CreateElement("Items");
foreach (DynamicItem dynamicItem in Items) items.AppendChild(dynamicItem.GetXmlElement(doc));
item.AppendChild(items);
return item;
}
}
public class HorizontalCardListItem : DynamicItem
{
public DynamicItem[] Items;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("CardList");
item.SetAttribute("title", Title);
foreach (DynamicItem dynamicItem in Items) item.AppendChild(dynamicItem.GetXmlElement(doc));
return item;
}
}
public class CardItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Card");
item.SetAttribute("title", Title);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class PlaylistVideoItem : DynamicItem
{
public long Index;
public Channel Channel;
public string Duration;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("index", Index.ToString());
item.SetAttribute("duration", Duration);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
public class ItemSectionItem : DynamicItem
{
public DynamicItem[] Contents;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement section = doc.CreateElement("ItemSection");
foreach (DynamicItem item in Contents) section.AppendChild(item.GetXmlElement(doc));
return section;
}
}
public class MessageItem : DynamicItem
{
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement message = doc.CreateElement("Message");
message.InnerText = Title;
return message;
}
}
public class ChannelAboutItem : DynamicItem
{
public string Description;
public string Country;
public string Joined;
public string ViewCount;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement about = doc.CreateElement("About");
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
about.AppendChild(description);
XmlElement country = doc.CreateElement("Location");
country.InnerText = Country;
about.AppendChild(country);
XmlElement joined = doc.CreateElement("Joined");
joined.InnerText = Joined;
about.AppendChild(joined);
XmlElement viewCount = doc.CreateElement("ViewCount");
viewCount.InnerText = ViewCount;
about.AppendChild(viewCount);
return about;
}
}
public class BadgeItem : DynamicItem
{
public string Style;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement badge = doc.CreateElement("Badge");
badge.SetAttribute("style", Style);
badge.InnerText = Title;
return badge;
}
}
public class StationItem : DynamicItem
{
public int VideoCount;
public string FirstVideoId;
public string Description;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Station");
item.SetAttribute("id", Id);
item.SetAttribute("videoCount", VideoCount.ToString());
item.SetAttribute("firstVideoId", FirstVideoId);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
}

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace InnerTube.Models
{
public class RequestContext
{
[JsonProperty("context")] public Context Context;
public static string BuildRequestContextJson(Dictionary<string, object> additionalFields, string language = "en",
string region = "US", string clientName = "WEB", string clientVersion = "2.20220224.07.00")
{
RequestContext ctx = new()
{
Context = new Context(
new RequestClient(language, region, clientName, clientVersion),
new RequestUser(false))
};
string json1 = JsonConvert.SerializeObject(ctx);
Dictionary<string, object> json2 = JsonConvert.DeserializeObject<Dictionary<string, object>>(json1);
foreach (KeyValuePair<string,object> pair in additionalFields) json2.Add(pair.Key, pair.Value);
return JsonConvert.SerializeObject(json2);
}
}
public class Context
{
[JsonProperty("client")] public RequestClient RequestClient { get; set; }
[JsonProperty("user")] public RequestUser RequestUser { get; set; }
public Context(RequestClient requestClient, RequestUser requestUser)
{
RequestClient = requestClient;
RequestUser = requestUser;
}
}
public class RequestClient
{
[JsonProperty("hl")] public string Language { get; set; }
[JsonProperty("gl")] public string Region { get; set; }
[JsonProperty("clientName")] public string ClientName { get; set; }
[JsonProperty("clientVersion")] public string ClientVersion { get; set; }
[JsonProperty("deviceModel")] public string DeviceModel { get; set; }
public RequestClient(string language, string region, string clientName, string clientVersion)
{
Language = language;
Region = region;
ClientName = clientName;
ClientVersion = clientVersion;
if (clientName == "IOS") DeviceModel = "iPhone14,3";
}
}
public class RequestUser
{
[JsonProperty("lockedSafetyMode")] public bool LockedSafetyMode { get; set; }
public RequestUser(bool lockedSafetyMode)
{
LockedSafetyMode = lockedSafetyMode;
}
}
}

View File

@@ -0,0 +1,71 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeChannel
{
public string Id;
public string Name;
public string Url;
public Thumbnail[] Avatars;
public Thumbnail[] Banners;
public string Description;
public DynamicItem[] Videos;
public string Subscribers;
public string GetHtmlDescription()
{
return Utils.GetHtmlDescription(Description);
}
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement channel = doc.CreateElement("Channel");
channel.SetAttribute("id", Id);
if (Id != Url)
channel.SetAttribute("customUrl", Url);
XmlElement metadata = doc.CreateElement("Metadata");
XmlElement name = doc.CreateElement("Name");
name.InnerText = Name;
metadata.AppendChild(name);
XmlElement avatars = doc.CreateElement("Avatars");
foreach (Thumbnail t in Avatars)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
avatars.AppendChild(thumbnail);
}
metadata.AppendChild(avatars);
XmlElement banners = doc.CreateElement("Banners");
foreach (Thumbnail t in Banners)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
banners.AppendChild(thumbnail);
}
metadata.AppendChild(banners);
XmlElement subscriberCount = doc.CreateElement("Subscribers");
subscriberCount.InnerText = Subscribers;
metadata.AppendChild(subscriberCount);
channel.AppendChild(metadata);
XmlElement contents = doc.CreateElement("Contents");
foreach (DynamicItem item in Videos) contents.AppendChild(item.GetXmlElement(doc));
channel.AppendChild(contents);
doc.AppendChild(channel);
return doc;
}
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeLocals
{
public Dictionary<string, string> Languages { get; set; }
public Dictionary<string, string> Regions { get; set; }
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement locals = doc.CreateElement("Locals");
XmlElement languages = doc.CreateElement("Languages");
foreach (KeyValuePair<string, string> l in Languages)
{
XmlElement language = doc.CreateElement("Language");
language.SetAttribute("hl", l.Key);
language.InnerText = l.Value;
languages.AppendChild(language);
}
locals.AppendChild(languages);
XmlElement regions = doc.CreateElement("Regions");
foreach (KeyValuePair<string, string> r in Regions)
{
XmlElement region = doc.CreateElement("Region");
region.SetAttribute("gl", r.Key);
region.InnerText = r.Value;
regions.AppendChild(region);
}
locals.AppendChild(regions);
doc.AppendChild(locals);
return doc;
}
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Xml;
using Newtonsoft.Json;
namespace InnerTube.Models
{
public class YoutubePlayer
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string[] Tags { get; set; }
public Channel Channel { get; set; }
public long? Duration { get; set; }
public bool IsLive { get; set; }
public Chapter[] Chapters { get; set; }
public Thumbnail[] Thumbnails { get; set; }
public Format[] Formats { get; set; }
public Format[] AdaptiveFormats { get; set; }
public string HlsManifestUrl { get; set; }
public Subtitle[] Subtitles { get; set; }
public string[] Storyboards { get; set; }
public string ExpiresInSeconds { get; set; }
public string ErrorMessage { get; set; }
public string GetHtmlDescription()
{
return Utils.GetHtmlDescription(Description);
}
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
XmlElement error = doc.CreateElement("Error");
error.InnerText = ErrorMessage;
doc.AppendChild(error);
}
else
{
XmlElement player = doc.CreateElement("Player");
player.SetAttribute("id", Id);
player.SetAttribute("duration", Duration.ToString());
player.SetAttribute("isLive", IsLive.ToString());
player.SetAttribute("expiresInSeconds", ExpiresInSeconds);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
player.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
player.AppendChild(description);
XmlElement tags = doc.CreateElement("Tags");
foreach (string tag in Tags ?? Array.Empty<string>())
{
XmlElement tagElement = doc.CreateElement("Tag");
tagElement.InnerText = tag;
tags.AppendChild(tagElement);
}
player.AppendChild(tags);
player.AppendChild(Channel.GetXmlElement(doc));
XmlElement thumbnails = doc.CreateElement("Thumbnails");
foreach (Thumbnail t in Thumbnails)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
thumbnails.AppendChild(thumbnail);
}
player.AppendChild(thumbnails);
XmlElement formats = doc.CreateElement("Formats");
foreach (Format f in Formats ?? Array.Empty<Format>()) formats.AppendChild(f.GetXmlElement(doc));
player.AppendChild(formats);
XmlElement adaptiveFormats = doc.CreateElement("AdaptiveFormats");
foreach (Format f in AdaptiveFormats ?? Array.Empty<Format>()) adaptiveFormats.AppendChild(f.GetXmlElement(doc));
player.AppendChild(adaptiveFormats);
XmlElement storyboards = doc.CreateElement("Storyboards");
foreach (string s in Storyboards)
{
XmlElement storyboard = doc.CreateElement("Storyboard");
storyboard.InnerText = s;
storyboards.AppendChild(storyboard);
}
player.AppendChild(storyboards);
XmlElement subtitles = doc.CreateElement("Subtitles");
foreach (Subtitle s in Subtitles ?? Array.Empty<Subtitle>()) subtitles.AppendChild(s.GetXmlElement(doc));
player.AppendChild(subtitles);
doc.AppendChild(player);
}
return doc;
}
}
public class Chapter
{
[JsonProperty("title")] public string Title { get; set; }
[JsonProperty("start_time")] public long StartTime { get; set; }
[JsonProperty("end_time")] public long EndTime { get; set; }
}
public class Format
{
[JsonProperty("format")] public string FormatName { get; set; }
[JsonProperty("format_id")] public string FormatId { get; set; }
[JsonProperty("format_note")] public string FormatNote { get; set; }
[JsonProperty("filesize")] public long? Filesize { get; set; }
[JsonProperty("quality")] public long Quality { get; set; }
[JsonProperty("bitrate")] public double Bitrate { get; set; }
[JsonProperty("audio_codec")] public string AudioCodec { get; set; }
[JsonProperty("video_codec")] public string VideoCodec { get; set; }
[JsonProperty("audio_sample_rate")] public long? AudioSampleRate { get; set; }
[JsonProperty("resolution")] public string Resolution { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("init_range")] public Range InitRange { get; set; }
[JsonProperty("index_range")] public Range IndexRange { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement format = doc.CreateElement("Format");
format.SetAttribute("id", FormatId);
format.SetAttribute("label", FormatName);
format.SetAttribute("filesize", Filesize.ToString());
format.SetAttribute("quality", Bitrate.ToString());
format.SetAttribute("audioCodec", AudioCodec);
format.SetAttribute("videoCodec", VideoCodec);
if (AudioSampleRate != null)
format.SetAttribute("audioSampleRate", AudioSampleRate.ToString());
else
format.SetAttribute("resolution", Resolution);
XmlElement url = doc.CreateElement("URL");
url.InnerText = Url;
format.AppendChild(url);
if (InitRange != null && IndexRange != null)
{
XmlElement initRange = doc.CreateElement("InitRange");
initRange.SetAttribute("start", InitRange.Start);
initRange.SetAttribute("end", InitRange.End);
format.AppendChild(initRange);
XmlElement indexRange = doc.CreateElement("IndexRange");
indexRange.SetAttribute("start", IndexRange.Start);
indexRange.SetAttribute("end", IndexRange.End);
format.AppendChild(indexRange);
}
return format;
}
}
public class Range
{
[JsonProperty("start")] public string Start { get; set; }
[JsonProperty("end")] public string End { get; set; }
public Range(string start, string end)
{
Start = start;
End = end;
}
}
public class Channel
{
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("subscriberCount")] public string SubscriberCount { get; set; }
[JsonProperty("avatars")] public Thumbnail[] Avatars { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement channel = doc.CreateElement("Channel");
channel.SetAttribute("id", Id);
if (!string.IsNullOrWhiteSpace(SubscriberCount))
channel.SetAttribute("subscriberCount", SubscriberCount);
XmlElement name = doc.CreateElement("Name");
name.InnerText = Name;
channel.AppendChild(name);
foreach (Thumbnail avatarThumb in Avatars ?? Array.Empty<Thumbnail>())
{
XmlElement avatar = doc.CreateElement("Avatar");
avatar.SetAttribute("width", avatarThumb.Width.ToString());
avatar.SetAttribute("height", avatarThumb.Height.ToString());
avatar.InnerText = avatarThumb.Url;
channel.AppendChild(avatar);
}
return channel;
}
}
public class Subtitle
{
[JsonProperty("ext")] public string Ext { get; set; }
[JsonProperty("name")] public string Language { get; set; }
[JsonProperty("url")] public string Url { get; set; }
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement subtitle = doc.CreateElement("Subtitle");
subtitle.SetAttribute("ext", Ext);
subtitle.SetAttribute("language", Language);
subtitle.InnerText = Url;
return subtitle;
}
}
public class Thumbnail
{
[JsonProperty("height")] public long Height { get; set; }
[JsonProperty("url")] public string Url { get; set; }
[JsonProperty("width")] public long Width { get; set; }
}
}

View File

@@ -0,0 +1,68 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubePlaylist
{
public string Id;
public string Title;
public string Description;
public string VideoCount;
public string ViewCount;
public string LastUpdated;
public Thumbnail[] Thumbnail;
public Channel Channel;
public DynamicItem[] Videos;
public string ContinuationKey;
public string GetHtmlDescription() => Utils.GetHtmlDescription(Description);
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement playlist = doc.CreateElement("Playlist");
playlist.SetAttribute("id", Id);
playlist.SetAttribute("continuation", ContinuationKey);
XmlElement metadata = doc.CreateElement("Metadata");
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
metadata.AppendChild(title);
metadata.AppendChild(Channel.GetXmlElement(doc));
XmlElement thumbnails = doc.CreateElement("Thumbnails");
foreach (Thumbnail t in Thumbnail)
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
thumbnails.AppendChild(thumbnail);
}
metadata.AppendChild(thumbnails);
XmlElement videoCount = doc.CreateElement("VideoCount");
XmlElement viewCount = doc.CreateElement("ViewCount");
XmlElement lastUpdated = doc.CreateElement("LastUpdated");
videoCount.InnerText = VideoCount;
viewCount.InnerText = ViewCount;
lastUpdated.InnerText = LastUpdated;
metadata.AppendChild(videoCount);
metadata.AppendChild(viewCount);
metadata.AppendChild(lastUpdated);
playlist.AppendChild(metadata);
XmlElement results = doc.CreateElement("Videos");
foreach (DynamicItem result in Videos) results.AppendChild(result.GetXmlElement(doc));
playlist.AppendChild(results);
doc.AppendChild(playlist);
return doc;
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeSearchResults
{
public string[] Refinements;
public long EstimatedResults;
public DynamicItem[] Results;
public string ContinuationKey;
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement search = doc.CreateElement("Search");
search.SetAttribute("estimatedResults", EstimatedResults.ToString());
search.SetAttribute("continuation", ContinuationKey);
if (Refinements.Length > 0)
{
XmlElement refinements = doc.CreateElement("Refinements");
foreach (string refinementText in Refinements)
{
XmlElement refinement = doc.CreateElement("Refinement");
refinement.InnerText = refinementText;
refinements.AppendChild(refinement);
}
search.AppendChild(refinements);
}
XmlElement results = doc.CreateElement("Results");
foreach (DynamicItem result in Results) results.AppendChild(result.GetXmlElement(doc));
search.AppendChild(results);
doc.AppendChild(search);
return doc;
}
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace InnerTube.Models
{
public class YoutubeStoryboardSpec
{
public Dictionary<string, string> Urls = new();
public YoutubeStoryboardSpec(string specStr, long duration)
{
if (specStr is null) return;
List<string> spec = new(specStr.Split("|"));
string baseUrl = spec[0];
spec.RemoveAt(0);
spec.Reverse();
int L = spec.Count - 1;
for (int i = 0; i < spec.Count; i++)
{
string[] args = spec[i].Split("#");
int width = int.Parse(args[0]);
int height = int.Parse(args[1]);
int frameCount = int.Parse(args[2]);
int cols = int.Parse(args[3]);
int rows = int.Parse(args[4]);
string N = args[6];
string sigh = args[7];
string url = baseUrl
.Replace("$L", (spec.Count - 1 - i).ToString())
.Replace("$N", N) + "&sigh=" + sigh;
float fragmentCount = frameCount / (cols * rows);
float fragmentDuration = duration / fragmentCount;
for (int j = 0; j < Math.Ceiling(fragmentCount); j++)
Urls.TryAdd($"L{spec.Count - 1 - i}", url.Replace("$M", j.ToString()));
}
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeTrends
{
public TrendCategory[] Categories;
public DynamicItem[] Videos;
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement explore = doc.CreateElement("Explore");
XmlElement categories = doc.CreateElement("Categories");
foreach (TrendCategory category in Categories ?? Array.Empty<TrendCategory>()) categories.AppendChild(category.GetXmlElement(doc));
explore.AppendChild(categories);
XmlElement contents = doc.CreateElement("Videos");
foreach (DynamicItem item in Videos ?? Array.Empty<DynamicItem>()) contents.AppendChild(item.GetXmlElement(doc));
explore.AppendChild(contents);
doc.AppendChild(explore);
return doc;
}
}
public class TrendCategory
{
public string Label;
public Thumbnail[] BackgroundImage;
public Thumbnail[] Icon;
public string Id;
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement category = doc.CreateElement("Category");
category.SetAttribute("id", Id);
XmlElement title = doc.CreateElement("Name");
title.InnerText = Label;
category.AppendChild(title);
XmlElement backgroundImages = doc.CreateElement("BackgroundImage");
foreach (Thumbnail t in BackgroundImage ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
backgroundImages.AppendChild(thumbnail);
}
category.AppendChild(backgroundImages);
XmlElement icons = doc.CreateElement("Icon");
foreach (Thumbnail t in Icon ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
icons.AppendChild(thumbnail);
}
category.AppendChild(icons);
return category;
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Xml;
namespace InnerTube.Models
{
public class YoutubeVideo
{
public string Id;
public string Title;
public string Description;
public Channel Channel;
public string UploadDate;
public DynamicItem[] Recommended;
public string Views;
public string GetHtmlDescription() => InnerTube.Utils.GetHtmlDescription(Description);
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("views", Views);
item.SetAttribute("uploadDate", UploadDate);
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
item.AppendChild(Channel.GetXmlElement(doc));
XmlElement recommendations = doc.CreateElement("Recommendations");
foreach (DynamicItem f in Recommended ?? Array.Empty<DynamicItem>()) recommendations.AppendChild(f.GetXmlElement(doc));
item.AppendChild(recommendations);
doc.AppendChild(item);
return doc;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace InnerTube
{
public static class ReturnYouTubeDislike
{
private static HttpClient _client = new();
private static Dictionary<string, YoutubeDislikes> DislikesCache = new();
// TODO: better cache
public static async Task<YoutubeDislikes> GetDislikes(string videoId)
{
if (DislikesCache.ContainsKey(videoId))
return DislikesCache[videoId];
HttpResponseMessage response = await _client.GetAsync("https://returnyoutubedislikeapi.com/votes?videoId=" + videoId);
string json = await response.Content.ReadAsStringAsync();
YoutubeDislikes dislikes = JsonConvert.DeserializeObject<YoutubeDislikes>(json);
if (dislikes is not null)
DislikesCache.Add(videoId, dislikes);
return dislikes ?? new YoutubeDislikes();
}
}
public class YoutubeDislikes
{
[JsonProperty("id")] public string Id { get; set; }
[JsonProperty("dateCreated")] public string DateCreated { get; set; }
[JsonProperty("likes")] public long Likes { get; set; }
[JsonProperty("dislikes")] public long Dislikes { get; set; }
[JsonProperty("rating")] public double Rating { get; set; }
[JsonProperty("viewCount")] public long Views { get; set; }
[JsonProperty("deleted")] public bool Deleted { get; set; }
public float GetLikePercentage()
{
return Likes / (float)(Likes + Dislikes) * 100;
}
}
}

457
core/InnerTube/Utils.cs Normal file
View File

@@ -0,0 +1,457 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using InnerTube.Models;
using Newtonsoft.Json.Linq;
namespace InnerTube
{
public static class Utils
{
private static string Sapisid;
private static string Psid;
private static bool UseAuthorization;
public static string GetHtmlDescription(string description) => description?.Replace("\n", "<br>") ?? "";
public static string GetMpdManifest(this YoutubePlayer player, string proxyUrl, string videoCodec = null, string audioCodec = null)
{
XmlDocument doc = new();
XmlDeclaration xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
XmlElement root = doc.DocumentElement;
doc.InsertBefore(xmlDeclaration, root);
XmlElement mpdRoot = doc.CreateElement(string.Empty, "MPD", string.Empty);
mpdRoot.SetAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
mpdRoot.SetAttribute("xmlns", "urn:mpeg:dash:schema:mpd:2011");
mpdRoot.SetAttribute("xsi:schemaLocation", "urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd");
//mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-on-demand:2011");
mpdRoot.SetAttribute("profiles", "urn:mpeg:dash:profile:isoff-main:2011");
mpdRoot.SetAttribute("type", "static");
mpdRoot.SetAttribute("minBufferTime", "PT1.500S");
TimeSpan durationTs = TimeSpan.FromMilliseconds(double.Parse(HttpUtility
.ParseQueryString(player.Formats.First().Url.Split("?")[1])
.Get("dur")?.Replace(".", "") ?? "0"));
StringBuilder duration = new("PT");
if (durationTs.TotalHours > 0)
duration.Append($"{durationTs.Hours}H");
if (durationTs.Minutes > 0)
duration.Append($"{durationTs.Minutes}M");
if (durationTs.Seconds > 0)
duration.Append(durationTs.Seconds);
mpdRoot.SetAttribute("mediaPresentationDuration", $"{duration}.{durationTs.Milliseconds}S");
doc.AppendChild(mpdRoot);
XmlElement period = doc.CreateElement("Period");
period.AppendChild(doc.CreateComment("Audio Adaptation Set"));
XmlElement audioAdaptationSet = doc.CreateElement("AdaptationSet");
List<Format> audios;
if (audioCodec != "all")
audios = player.AdaptiveFormats
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17" &&
(audioCodec == null || x.AudioCodec.ToLower().Contains(audioCodec.ToLower())))
.GroupBy(x => x.FormatNote)
.Select(x => x.Last())
.ToList();
else
audios = player.AdaptiveFormats
.Where(x => x.AudioSampleRate.HasValue && x.FormatId != "17")
.ToList();
audioAdaptationSet.SetAttribute("mimeType",
HttpUtility.ParseQueryString(audios.First().Url.Split("?")[1]).Get("mime"));
audioAdaptationSet.SetAttribute("subsegmentAlignment", "true");
audioAdaptationSet.SetAttribute("contentType", "audio");
foreach (Format format in audios)
{
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", format.FormatId);
representation.SetAttribute("codecs", format.AudioCodec);
representation.SetAttribute("startWithSAP", "1");
representation.SetAttribute("bandwidth",
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
XmlElement audioChannelConfiguration = doc.CreateElement("AudioChannelConfiguration");
audioChannelConfiguration.SetAttribute("schemeIdUri",
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011");
audioChannelConfiguration.SetAttribute("value", "2");
representation.AppendChild(audioChannelConfiguration);
XmlElement baseUrl = doc.CreateElement("BaseURL");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
representation.AppendChild(baseUrl);
if (format.IndexRange != null && format.InitRange != null)
{
XmlElement segmentBase = doc.CreateElement("SegmentBase");
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
segmentBase.SetAttribute("indexRangeExact", "true");
XmlElement initialization = doc.CreateElement("Initialization");
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
segmentBase.AppendChild(initialization);
representation.AppendChild(segmentBase);
}
audioAdaptationSet.AppendChild(representation);
}
period.AppendChild(audioAdaptationSet);
period.AppendChild(doc.CreateComment("Video Adaptation Set"));
List<Format> videos;
if (videoCodec != "all")
videos = player.AdaptiveFormats.Where(x => !x.AudioSampleRate.HasValue && x.FormatId != "17" &&
(videoCodec == null || x.VideoCodec.ToLower()
.Contains(videoCodec.ToLower())))
.GroupBy(x => x.FormatNote)
.Select(x => x.Last())
.ToList();
else
videos = player.AdaptiveFormats.Where(x => x.Resolution != "audio only" && x.FormatId != "17").ToList();
XmlElement videoAdaptationSet = doc.CreateElement("AdaptationSet");
videoAdaptationSet.SetAttribute("mimeType",
HttpUtility.ParseQueryString(videos.FirstOrDefault()?.Url?.Split("?")[1] ?? "mime=video/mp4")
.Get("mime"));
videoAdaptationSet.SetAttribute("subsegmentAlignment", "true");
videoAdaptationSet.SetAttribute("contentType", "video");
foreach (Format format in videos)
{
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", format.FormatId);
representation.SetAttribute("codecs", format.VideoCodec);
representation.SetAttribute("startWithSAP", "1");
string[] widthAndHeight = format.Resolution.Split("x");
representation.SetAttribute("width", widthAndHeight[0]);
representation.SetAttribute("height", widthAndHeight[1]);
representation.SetAttribute("bandwidth",
Math.Floor((format.Filesize ?? 1) / (double)player.Duration).ToString());
XmlElement baseUrl = doc.CreateElement("BaseURL");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? format.Url : $"{proxyUrl}media/{player.Id}/{format.FormatId}";
representation.AppendChild(baseUrl);
if (format.IndexRange != null && format.InitRange != null)
{
XmlElement segmentBase = doc.CreateElement("SegmentBase");
segmentBase.SetAttribute("indexRange", $"{format.IndexRange.Start}-{format.IndexRange.End}");
segmentBase.SetAttribute("indexRangeExact", "true");
XmlElement initialization = doc.CreateElement("Initialization");
initialization.SetAttribute("range", $"{format.InitRange.Start}-{format.InitRange.End}");
segmentBase.AppendChild(initialization);
representation.AppendChild(segmentBase);
}
videoAdaptationSet.AppendChild(representation);
}
period.AppendChild(videoAdaptationSet);
period.AppendChild(doc.CreateComment("Subtitle Adaptation Sets"));
foreach (Subtitle subtitle in player.Subtitles ?? Array.Empty<Subtitle>())
{
period.AppendChild(doc.CreateComment(subtitle.Language));
XmlElement adaptationSet = doc.CreateElement("AdaptationSet");
adaptationSet.SetAttribute("mimeType", "text/vtt");
adaptationSet.SetAttribute("lang", subtitle.Language);
XmlElement representation = doc.CreateElement("Representation");
representation.SetAttribute("id", $"caption_{subtitle.Language.ToLower()}");
representation.SetAttribute("bandwidth", "256"); // ...why do we need this for a plaintext file
XmlElement baseUrl = doc.CreateElement("BaseURL");
string url = subtitle.Url;
url = url.Replace("fmt=srv3", "fmt=vtt");
baseUrl.InnerText = string.IsNullOrWhiteSpace(proxyUrl) ? url : $"{proxyUrl}caption/{player.Id}/{subtitle.Language}";
representation.AppendChild(baseUrl);
adaptationSet.AppendChild(representation);
period.AppendChild(adaptationSet);
}
mpdRoot.AppendChild(period);
return doc.OuterXml.Replace(" schemaLocation=\"", " xsi:schemaLocation=\"");
}
public static async Task<string> GetHlsManifest(this YoutubePlayer player, string proxyUrl)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("#EXTM3U");
sb.AppendLine("##Generated by LightTube");
sb.AppendLine("##Video ID: " + player.Id);
sb.AppendLine("#EXT-X-VERSION:7");
sb.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS");
string hls = await new HttpClient().GetStringAsync(player.HlsManifestUrl);
string[] hlsLines = hls.Split("\n");
foreach (string line in hlsLines)
{
if (line.StartsWith("#EXT-X-STREAM-INF:"))
sb.AppendLine(line);
if (line.StartsWith("http"))
{
Uri u = new(line);
sb.AppendLine($"{proxyUrl}/ytmanifest?path={HttpUtility.UrlEncode(u.PathAndQuery)}");
}
}
return sb.ToString();
}
public static string ReadRuns(JArray runs)
{
string str = "";
foreach (JToken runToken in runs ?? new JArray())
{
JObject run = runToken as JObject;
if (run is null) continue;
if (run.ContainsKey("bold"))
{
str += "<b>" + run["text"] + "</b>";
}
else if (run.ContainsKey("navigationEndpoint"))
{
if (run?["navigationEndpoint"]?["urlEndpoint"] is not null)
{
string url = run["navigationEndpoint"]?["urlEndpoint"]?["url"]?.ToString() ?? "";
if (url.StartsWith("https://www.youtube.com/redirect"))
{
NameValueCollection qsl = HttpUtility.ParseQueryString(url.Split("?")[1]);
url = qsl["url"] ?? qsl["q"];
}
str += $"<a href=\"{url}\">{run["text"]}</a>";
}
else if (run?["navigationEndpoint"]?["commandMetadata"] is not null)
{
string url = run["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
?.ToString() ?? "";
if (url.StartsWith("/"))
url = "https://youtube.com" + url;
str += $"<a href=\"{url}\">{run["text"]}</a>";
}
}
else
{
str += run["text"];
}
}
return str;
}
public static Thumbnail ParseThumbnails(JToken arg) => new()
{
Height = arg["height"]?.ToObject<long>() ?? -1,
Url = arg["url"]?.ToString() ?? string.Empty,
Width = arg["width"]?.ToObject<long>() ?? -1
};
public static async Task<JObject> GetAuthorizedPlayer(string id, HttpClient client)
{
HttpRequestMessage hrm = new(HttpMethod.Post,
"https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
byte[] buffer = Encoding.UTF8.GetBytes(
RequestContext.BuildRequestContextJson(new Dictionary<string, object>
{
["videoId"] = id
}));
ByteArrayContent byteContent = new(buffer);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
hrm.Content = byteContent;
if (UseAuthorization)
{
hrm.Headers.Add("Cookie", GenerateAuthCookie());
hrm.Headers.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0");
hrm.Headers.Add("Authorization", GenerateAuthHeader());
hrm.Headers.Add("X-Origin", "https://www.youtube.com");
hrm.Headers.Add("X-Youtube-Client-Name", "1");
hrm.Headers.Add("X-Youtube-Client-Version", "2.20210721.00.00");
hrm.Headers.Add("Accept-Language", "en-US;q=0.8,en;q=0.7");
hrm.Headers.Add("Origin", "https://www.youtube.com");
hrm.Headers.Add("Referer", "https://www.youtube.com/watch?v=" + id);
}
HttpResponseMessage ytPlayerRequest = await client.SendAsync(hrm);
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
}
internal static string GenerateAuthHeader()
{
if (!UseAuthorization) return "None none";
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string hashInput = timestamp + " " + Sapisid + " https://www.youtube.com";
string hashDigest = GenerateSha1Hash(hashInput);
return $"SAPISIDHASH {timestamp}_{hashDigest}";
}
internal static string GenerateAuthCookie() => UseAuthorization ? $"SAPISID={Sapisid}; __Secure-3PAPISID={Sapisid}; __Secure-3PSID={Psid};" : ";";
private static string GenerateSha1Hash(string input)
{
using SHA1Managed sha1 = new();
byte[] hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
StringBuilder sb = new(hash.Length * 2);
foreach (byte b in hash) sb.Append(b.ToString("X2"));
return sb.ToString();
}
public static string GetExtension(this Format format)
{
if (format.VideoCodec != "none") return "mp4";
else
switch (format.FormatId)
{
case "139":
case "140":
case "141":
case "256":
case "258":
case "327":
return "mp3";
case "249":
case "250":
case "251":
case "338":
return "opus";
}
return "mp4";
}
public static void SetAuthorization(bool canUseAuthorizedEndpoints, string sapisid, string psid)
{
UseAuthorization = canUseAuthorizedEndpoints;
Sapisid = sapisid;
Psid = psid;
}
internal static string GetCodec(string mimetypeString, bool audioCodec)
{
string acodec = "";
string vcodec = "";
Match match = Regex.Match(mimetypeString, "codecs=\"([\\s\\S]+?)\"");
string[] g = match.Groups[1].ToString().Split(",");
foreach (string codec in g)
{
switch (codec.Split(".")[0].Trim())
{
case "avc1":
case "av01":
case "vp9":
case "mp4v":
vcodec = codec;
break;
case "mp4a":
case "opus":
acodec = codec;
break;
default:
Console.WriteLine("Unknown codec type: " + codec.Split(".")[0].Trim());
break;
}
}
return (audioCodec ? acodec : vcodec).Trim();
}
public static string GetFormatName(JToken formatToken)
{
string format = formatToken["itag"]?.ToString() switch
{
"160" => "144p",
"278" => "144p",
"330" => "144p",
"394" => "144p",
"694" => "144p",
"133" => "240p",
"242" => "240p",
"331" => "240p",
"395" => "240p",
"695" => "240p",
"134" => "360p",
"243" => "360p",
"332" => "360p",
"396" => "360p",
"696" => "360p",
"135" => "480p",
"244" => "480p",
"333" => "480p",
"397" => "480p",
"697" => "480p",
"136" => "720p",
"247" => "720p",
"298" => "720p",
"302" => "720p",
"334" => "720p",
"398" => "720p",
"698" => "720p",
"137" => "1080p",
"299" => "1080p",
"248" => "1080p",
"303" => "1080p",
"335" => "1080p",
"399" => "1080p",
"699" => "1080p",
"264" => "1440p",
"271" => "1440p",
"304" => "1440p",
"308" => "1440p",
"336" => "1440p",
"400" => "1440p",
"700" => "1440p",
"266" => "2160p",
"305" => "2160p",
"313" => "2160p",
"315" => "2160p",
"337" => "2160p",
"401" => "2160p",
"701" => "2160p",
"138" => "4320p",
"272" => "4320p",
"402" => "4320p",
"571" => "4320p",
var _ => $"{formatToken["height"]}p",
};
return format == "p"
? formatToken["audioQuality"]?.ToString().ToLowerInvariant()
: (formatToken["fps"]?.ToObject<int>() ?? 0) > 30
? $"{format}{formatToken["fps"]}"
: format;
}
}
}

790
core/InnerTube/Youtube.cs Normal file
View File

@@ -0,0 +1,790 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using InnerTube.Models;
using Newtonsoft.Json.Linq;
namespace InnerTube
{
public class Youtube
{
internal readonly HttpClient Client = new();
public readonly Dictionary<string, CacheItem<YoutubePlayer>> PlayerCache = new();
private readonly Dictionary<ChannelTabs, string> ChannelTabParams = new()
{
[ChannelTabs.Home] = @"EghmZWF0dXJlZA%3D%3D",
[ChannelTabs.Videos] = @"EgZ2aWRlb3M%3D",
[ChannelTabs.Playlists] = @"EglwbGF5bGlzdHM%3D",
[ChannelTabs.Community] = @"Egljb21tdW5pdHk%3D",
[ChannelTabs.Channels] = @"EghjaGFubmVscw%3D%3D",
[ChannelTabs.About] = @"EgVhYm91dA%3D%3D"
};
private async Task<JObject> MakeRequest(string endpoint, Dictionary<string, object> postData, string language,
string region, string clientName = "WEB", string clientId = "1", string clientVersion = "2.20220405", bool authorized = false)
{
HttpRequestMessage hrm = new(HttpMethod.Post,
@$"https://www.youtube.com/youtubei/v1/{endpoint}?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8");
byte[] buffer = Encoding.UTF8.GetBytes(RequestContext.BuildRequestContextJson(postData, language, region, clientName, clientVersion));
ByteArrayContent byteContent = new(buffer);
if (authorized)
{
hrm.Headers.Add("Cookie", Utils.GenerateAuthCookie());
hrm.Headers.Add("Authorization", Utils.GenerateAuthHeader());
hrm.Headers.Add("X-Youtube-Client-Name", clientId);
hrm.Headers.Add("X-Youtube-Client-Version", clientVersion);
hrm.Headers.Add("Origin", "https://www.youtube.com");
}
byteContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
hrm.Content = byteContent;
HttpResponseMessage ytPlayerRequest = await Client.SendAsync(hrm);
return JObject.Parse(await ytPlayerRequest.Content.ReadAsStringAsync());
}
public async Task<YoutubePlayer> GetPlayerAsync(string videoId, string language = "en", string region = "US", bool iOS = false)
{
if (PlayerCache.Any(x => x.Key == videoId && x.Value.ExpireTime > DateTimeOffset.Now))
{
CacheItem<YoutubePlayer> item = PlayerCache[videoId];
item.Item.ExpiresInSeconds = ((int)(item.ExpireTime - DateTimeOffset.Now).TotalSeconds).ToString();
return item.Item;
}
JObject player = await MakeRequest("player", new Dictionary<string, object>
{
["videoId"] = videoId,
["contentCheckOk"] = true,
["racyCheckOk"] = true
}, language, region, iOS ? "IOS" : "ANDROID", iOS ? "5" : "3", "17.13.3", true);
switch (player["playabilityStatus"]?["status"]?.ToString())
{
case "OK":
YoutubeStoryboardSpec storyboardSpec =
new(player["storyboards"]?["playerStoryboardSpecRenderer"]?["spec"]?.ToString(), player["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0);
YoutubePlayer video = new()
{
Id = player["videoDetails"]?["videoId"]?.ToString(),
Title = player["videoDetails"]?["title"]?.ToString(),
Description = player["videoDetails"]?["shortDescription"]?.ToString(),
Tags = player["videoDetails"]?["keywords"]?.ToObject<string[]>(),
Channel = new Channel
{
Name = player["videoDetails"]?["author"]?.ToString(),
Id = player["videoDetails"]?["channelId"]?.ToString(),
Avatars = Array.Empty<Thumbnail>()
},
Duration = player["videoDetails"]?["lengthSeconds"]?.ToObject<long>(),
IsLive = player["videoDetails"]?["isLiveContent"]?.ToObject<bool>() ?? false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = player["videoDetails"]?["thumbnail"]?["thumbnails"]?.Select(x => new Thumbnail
{
Height = x["height"]?.ToObject<int>() ?? -1,
Url = x["url"]?.ToString(),
Width = x["width"]?.ToObject<int>() ?? -1
}).ToArray(),
Formats = player["streamingData"]?["formats"]?.Select(x => new Format
{
FormatName = Utils.GetFormatName(x),
FormatId = x["itag"]?.ToString(),
FormatNote = x["quality"]?.ToString(),
Filesize = x["contentLength"]?.ToObject<long>(),
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
AudioCodec = Utils.GetCodec(x["mimeType"]?.ToString(), true),
VideoCodec = Utils.GetCodec(x["mimeType"]?.ToString(), false),
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
Url = x["url"]?.ToString()
}).ToArray() ?? Array.Empty<Format>(),
AdaptiveFormats = player["streamingData"]?["adaptiveFormats"]?.Select(x => new Format
{
FormatName = Utils.GetFormatName(x),
FormatId = x["itag"]?.ToString(),
FormatNote = x["quality"]?.ToString(),
Filesize = x["contentLength"]?.ToObject<long>(),
Bitrate = x["bitrate"]?.ToObject<long>() ?? 0,
AudioCodec = Utils.GetCodec(x["mimeType"].ToString(), true),
VideoCodec = Utils.GetCodec(x["mimeType"].ToString(), false),
AudioSampleRate = x["audioSampleRate"]?.ToObject<long>(),
Resolution = $"{x["width"] ?? "0"}x{x["height"] ?? "0"}",
Url = x["url"]?.ToString(),
InitRange = x["initRange"]?.ToObject<Models.Range>(),
IndexRange = x["indexRange"]?.ToObject<Models.Range>()
}).ToArray() ?? Array.Empty<Format>(),
HlsManifestUrl = player["streamingData"]?["hlsManifestUrl"]?.ToString(),
Subtitles = player["captions"]?["playerCaptionsTracklistRenderer"]?["captionTracks"]?.Select(
x => new Subtitle
{
Ext = HttpUtility.ParseQueryString(x["baseUrl"].ToString()).Get("fmt"),
Language = Utils.ReadRuns(x["name"]?["runs"]?.ToObject<JArray>()),
Url = x["baseUrl"].ToString()
}).ToArray(),
Storyboards = storyboardSpec.Urls.TryGetValue("L0", out string sb) ? new[] { sb } : Array.Empty<string>(),
ExpiresInSeconds = player["streamingData"]?["expiresInSeconds"]?.ToString(),
ErrorMessage = null
};
PlayerCache.Remove(videoId);
PlayerCache.Add(videoId,
new CacheItem<YoutubePlayer>(video,
TimeSpan.FromSeconds(int.Parse(video.ExpiresInSeconds ?? "21600"))
.Subtract(TimeSpan.FromHours(1))));
return video;
case "LOGIN_REQUIRED":
return new YoutubePlayer
{
Id = "",
Title = "",
Description = "",
Tags = Array.Empty<string>(),
Channel = new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
},
Duration = 0,
IsLive = false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = Array.Empty<Thumbnail>(),
Formats = Array.Empty<Format>(),
AdaptiveFormats = Array.Empty<Format>(),
Subtitles = Array.Empty<Subtitle>(),
Storyboards = Array.Empty<string>(),
ExpiresInSeconds = "0",
ErrorMessage =
"This video is age-restricted. Please contact this instances authors to update their configuration"
};
default:
return new YoutubePlayer
{
Id = "",
Title = "",
Description = "",
Tags = Array.Empty<string>(),
Channel = new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
},
Duration = 0,
IsLive = false,
Chapters = Array.Empty<Chapter>(),
Thumbnails = Array.Empty<Thumbnail>(),
Formats = Array.Empty<Format>(),
AdaptiveFormats = Array.Empty<Format>(),
Subtitles = Array.Empty<Subtitle>(),
Storyboards = Array.Empty<string>(),
ExpiresInSeconds = "0",
ErrorMessage = player["playabilityStatus"]?["reason"]?.ToString() ?? "Something has gone *really* wrong"
};
}
}
public async Task<YoutubeVideo> GetVideoAsync(string videoId, string language = "en", string region = "US")
{
JObject player = await MakeRequest("next", new Dictionary<string, object>
{
["videoId"] = videoId
}, language, region);
JToken[] contents =
(player?["contents"]?["twoColumnWatchNextResults"]?["results"]?["results"]?["contents"]
?.ToObject<JArray>() ?? new JArray())
.SkipWhile(x => !x.First.Path.EndsWith("videoPrimaryInfoRenderer")).ToArray();
YoutubeVideo video = new();
video.Id = player["currentVideoEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString();
try
{
video.Title = Utils.ReadRuns(
contents[0]
["videoPrimaryInfoRenderer"]?["title"]?["runs"]?.ToObject<JArray>());
video.Description = Utils.ReadRuns(
contents[1]
["videoSecondaryInfoRenderer"]?["description"]?["runs"]?.ToObject<JArray>());
video.Views = contents[0]
["videoPrimaryInfoRenderer"]?["viewCount"]?["videoViewCountRenderer"]?["viewCount"]?["simpleText"]?.ToString();
video.Channel = new Channel
{
Name =
contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?[
"text"]?.ToString(),
Id = contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["title"]?["runs"]?[0]?
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount =
contents[1]
["videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["subscriberCountText"]?[
"simpleText"]?.ToString(),
Avatars =
(contents[1][
"videoSecondaryInfoRenderer"]?["owner"]?["videoOwnerRenderer"]?["thumbnail"]?[
"thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
};
video.UploadDate = contents[0][
"videoPrimaryInfoRenderer"]?["dateText"]?["simpleText"]?.ToString();
}
catch
{
video.Title ??= "";
video.Description ??= "";
video.Channel ??= new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
};
video.UploadDate ??= "";
}
video.Recommended = ParseRenderers(
player?["contents"]?["twoColumnWatchNextResults"]?["secondaryResults"]?["secondaryResults"]?
["results"]?.ToObject<JArray>() ?? new JArray());
return video;
}
public async Task<YoutubeSearchResults> SearchAsync(string query, string continuation = null,
string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
data.Add("query", query);
else
data.Add("continuation", continuation);
JObject search = await MakeRequest("search", data, language, region);
return new YoutubeSearchResults
{
Refinements = search?["refinements"]?.ToObject<string[]>() ?? Array.Empty<string>(),
EstimatedResults = search?["estimatedResults"]?.ToObject<long>() ?? 0,
Results = ParseRenderers(
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
["contents"]?[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ??
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
[0]?["itemSectionRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()),
ContinuationKey =
search?["contents"]?["twoColumnSearchResultsRenderer"]?["primaryContents"]?["sectionListRenderer"]?
["contents"]?[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?
["token"]?.ToString() ??
search?["onResponseReceivedCommands"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]?
[1]?["continuationItemRenderer"]?["continuationEndpoint"]?["continuationCommand"]?["token"]
?.ToString() ?? ""
};
}
public async Task<YoutubePlaylist> GetPlaylistAsync(string id, string continuation = null,
string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
data.Add("browseId", "VL" + id);
else
data.Add("continuation", continuation);
JObject playlist = await MakeRequest("browse", data, language, region);
DynamicItem[] renderers = ParseRenderers(
playlist?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?
["playlistVideoListRenderer"]?["contents"]?.ToObject<JArray>() ??
playlist?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?["continuationItems"]
?.ToObject<JArray>() ?? new JArray());
return new YoutubePlaylist
{
Id = id,
Title = playlist?["metadata"]?["playlistMetadataRenderer"]?["title"]?.ToString(),
Description = playlist?["metadata"]?["playlistMetadataRenderer"]?["description"]?.ToString(),
VideoCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[0]?["runs"]?[0]?["text"]?.ToString(),
ViewCount = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[1]?["simpleText"]?.ToString(),
LastUpdated = Utils.ReadRuns(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[0]?[
"playlistSidebarPrimaryInfoRenderer"]?["stats"]?[2]?["runs"]?.ToObject<JArray>() ?? new JArray()),
Thumbnail = (playlist?["microformat"]?["microformatDataRenderer"]?["thumbnail"]?["thumbnails"] ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Channel = new Channel
{
Name =
playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["title"]?
["runs"]?[0]?["text"]?.ToString(),
Id = playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?
["navigationEndpoint"]?["browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = "",
Avatars =
(playlist?["sidebar"]?["playlistSidebarRenderer"]?["items"]?[1]?
["playlistSidebarSecondaryInfoRenderer"]?["videoOwner"]?["videoOwnerRenderer"]?["thumbnail"]
?["thumbnails"] ?? new JArray()).Select(Utils.ParseThumbnails).ToArray()
},
Videos = renderers.Where(x => x is not ContinuationItem).ToArray(),
ContinuationKey = renderers.FirstOrDefault(x => x is ContinuationItem)?.Id
};
}
public async Task<YoutubeChannel> GetChannelAsync(string id, ChannelTabs tab = ChannelTabs.Home,
string continuation = null, string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
{
data.Add("browseId", id);
if (string.IsNullOrWhiteSpace(continuation))
data.Add("params", ChannelTabParams[tab]);
}
else
{
data.Add("continuation", continuation);
}
JObject channel = await MakeRequest("browse", data, language, region);
JArray mainArray =
(channel?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?.ToObject<JArray>() ?? new JArray())
.FirstOrDefault(x => x?["tabRenderer"]?["selected"]?.ToObject<bool>() ?? false)?["tabRenderer"]?[
"content"]?
["sectionListRenderer"]?["contents"]?.ToObject<JArray>();
return new YoutubeChannel
{
Id = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
Name = channel?["metadata"]?["channelMetadataRenderer"]?["title"]?.ToString(),
Url = channel?["metadata"]?["channelMetadataRenderer"]?["externalId"]?.ToString(),
Avatars = (channel?["metadata"]?["channelMetadataRenderer"]?["avatar"]?["thumbnails"] ?? new JArray())
.Select(Utils.ParseThumbnails).ToArray(),
Banners = (channel?["header"]?["c4TabbedHeaderRenderer"]?["banner"]?["thumbnails"] ?? new JArray())
.Select(Utils.ParseThumbnails).ToArray(),
Description = channel?["metadata"]?["channelMetadataRenderer"]?["description"]?.ToString(),
Videos = ParseRenderers(mainArray ??
channel?["onResponseReceivedActions"]?[0]?["appendContinuationItemsAction"]?
["continuationItems"]?.ToObject<JArray>() ?? new JArray()),
Subscribers = channel?["header"]?["c4TabbedHeaderRenderer"]?["subscriberCountText"]?["simpleText"]
?.ToString()
};
}
public async Task<YoutubeTrends> GetExploreAsync(string browseId = null, string continuation = null, string language = "en", string region = "US")
{
Dictionary<string, object> data = new();
if (string.IsNullOrWhiteSpace(continuation))
{
data.Add("browseId", browseId ?? "FEexplore");
}
else
{
data.Add("continuation", continuation);
}
JObject explore = await MakeRequest("browse", data, language, region);
JToken[] token =
(explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?
["sectionListRenderer"]?["contents"]?.ToObject<JArray>() ?? new JArray()).Skip(1).ToArray();
JArray mainArray = new(token.Select(x => x is JObject obj ? obj : null).Where(x => x is not null));
return new YoutubeTrends
{
Categories = explore?["contents"]?["twoColumnBrowseResultsRenderer"]?["tabs"]?[0]?["tabRenderer"]?["content"]?["sectionListRenderer"]?["contents"]?[0]?["itemSectionRenderer"]?["contents"]?[0]?["destinationShelfRenderer"]?["destinationButtons"]?.Select(
x =>
{
JToken rendererObject = x?["destinationButtonRenderer"];
TrendCategory category = new()
{
Label = rendererObject?["label"]?["simpleText"]?.ToString(),
BackgroundImage = (rendererObject?["backgroundImage"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Icon = (rendererObject?["iconImage"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Id = $"{rendererObject?["onTap"]?["browseEndpoint"]?["browseId"]}"
};
return category;
}).ToArray(),
Videos = ParseRenderers(mainArray)
};
}
public async Task<YoutubeLocals> GetLocalsAsync(string language = "en", string region = "US")
{
JObject locals = await MakeRequest("account/account_menu", new Dictionary<string, object>(), language,
region);
return new YoutubeLocals
{
Languages =
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
["multiPageMenuSectionRenderer"]?["items"]?[1]?["compactLinkRenderer"]?["serviceEndpoint"]?
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
.ToObject<JArray>()?.ToDictionary(
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
["actions"]?[0]?["selectLanguageCommand"]?["hl"]?.ToString(),
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString()),
Regions =
locals["actions"]?[0]?["openPopupAction"]?["popup"]?["multiPageMenuRenderer"]?["sections"]?[0]?
["multiPageMenuSectionRenderer"]?["items"]?[2]?["compactLinkRenderer"]?["serviceEndpoint"]?
["signalServiceEndpoint"]?["actions"]?[0]?["getMultiPageMenuAction"]?["menu"]?
["multiPageMenuRenderer"]?["sections"]?[0]?["multiPageMenuSectionRenderer"]?["items"]?
.ToObject<JArray>()?.ToDictionary(
x => x?["compactLinkRenderer"]?["serviceEndpoint"]?["signalServiceEndpoint"]?
["actions"]?[0]?["selectCountryCommand"]?["gl"]?.ToString(),
x => x?["compactLinkRenderer"]?["title"]?["simpleText"]?.ToString())
};
}
private DynamicItem[] ParseRenderers(JArray renderersArray)
{
List<DynamicItem> items = new();
foreach (JToken jToken in renderersArray)
{
JObject recommendationContainer = jToken as JObject;
string rendererName = recommendationContainer?.First?.Path.Split(".").Last() ?? "";
JObject rendererItem = recommendationContainer?[rendererName]?.ToObject<JObject>();
switch (rendererName)
{
case "videoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long vV) ? vV : 0,
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars =
(rendererItem?["channelThumbnailSupportedRenderers"]?[
"channelThumbnailWithLinkRenderer"]?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray()
},
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString(),
Description = Utils.ReadRuns(rendererItem?["detailedMetadataSnippets"]?[0]?[
"snippetText"]?["runs"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "gridVideoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString() ?? Utils.ReadRuns(
rendererItem?["title"]?["runs"]?.ToObject<JArray>() ?? new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long gVV) ? gVV : 0,
Channel = null,
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
});
break;
case "playlistRenderer":
items.Add(new PlaylistItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnails"]?[0]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
VideoCount = int.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
.Replace(".", "") ?? "0", out int pVC) ? pVC : 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]
?.ToString(),
SubscriberCount = null,
Avatars = null
}
});
break;
case "channelRenderer":
items.Add(new ChannelItem
{
Id = rendererItem?["channelId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails)
.ToArray(), //
Url = rendererItem?["navigationEndpoint"]?["commandMetadata"]?["webCommandMetadata"]?["url"]
?.ToString(),
Description =
Utils.ReadRuns(rendererItem?["descriptionSnippet"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
VideoCount = long.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]
?.ToString()
.Replace(",",
"")
.Replace(".",
"") ??
"0", out long cVC) ? cVC : 0,
Subscribers = rendererItem?["subscriberCountText"]?["simpleText"]?.ToString()
});
break;
case "radioRenderer":
items.Add(new RadioItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
Id = "",
SubscriberCount = null,
Avatars = null
}
});
break;
case "shelfRenderer":
items.Add(new ShelfItem
{
Title = rendererItem?["title"]?["simpleText"]
?.ToString() ??
rendererItem?["title"]?["runs"]?[0]?["text"]
?.ToString(),
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Items = ParseRenderers(
rendererItem?["content"]?["verticalListRenderer"]?["items"]
?.ToObject<JArray>() ??
rendererItem?["content"]?["horizontalListRenderer"]?["items"]
?.ToObject<JArray>() ??
rendererItem?["content"]?["expandedShelfContentsRenderer"]?["items"]
?.ToObject<JArray>() ??
new JArray()),
CollapsedItemCount =
rendererItem?["content"]?["verticalListRenderer"]?["collapsedItemCount"]
?.ToObject<int>() ?? 0,
Badges = ParseRenderers(rendererItem?["badges"]?.ToObject<JArray>() ?? new JArray())
.Where(x => x is BadgeItem).Cast<BadgeItem>().ToArray(),
});
break;
case "horizontalCardListRenderer":
items.Add(new HorizontalCardListItem
{
Title = rendererItem?["header"]?["richListHeaderRenderer"]?["title"]?["simpleText"]
?.ToString(),
Items = ParseRenderers(rendererItem?["cards"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "searchRefinementCardRenderer":
items.Add(new CardItem
{
Title = Utils.ReadRuns(rendererItem?["query"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails = (rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray()
});
break;
case "compactVideoRenderer":
items.Add(new VideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
UploadedAt = rendererItem?["publishedTimeText"]?["simpleText"]?.ToString(),
Views = long.TryParse(
rendererItem?["viewCountText"]?["simpleText"]?.ToString().Split(" ")[0]
.Replace(",", "").Replace(".", "") ?? "0", out long cVV) ? cVV : 0,
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars = null
},
Duration = rendererItem?["thumbnailOverlays"]?[0]?[
"thumbnailOverlayTimeStatusRenderer"]?["text"]?["simpleText"]?.ToString()
});
break;
case "compactPlaylistRenderer":
items.Add(new PlaylistItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray(),
VideoCount = int.TryParse(
rendererItem?["videoCountText"]?["runs"]?[0]?["text"]?.ToString().Replace(",", "")
.Replace(".", "") ?? "0", out int cPVC) ? cPVC : 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["runs"]?[0]?["text"]
?.ToString(),
Id = rendererItem?["longBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]
?.ToString(),
SubscriberCount = null,
Avatars = null
}
});
break;
case "compactRadioRenderer":
items.Add(new RadioItem
{
Id = rendererItem?["playlistId"]
?.ToString(),
Title = rendererItem?["title"]?["simpleText"]
?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]
?.ToObject<JArray>() ?? new JArray()).Select(Utils.ParseThumbnails)
.ToArray(),
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]
?.ToString(),
Channel = new Channel
{
Name = rendererItem?["longBylineText"]?["simpleText"]?.ToString(),
Id = "",
SubscriberCount = null,
Avatars = null
}
});
break;
case "continuationItemRenderer":
items.Add(new ContinuationItem
{
Id = rendererItem?["continuationEndpoint"]?["continuationCommand"]?["token"]?.ToString()
});
break;
case "playlistVideoRenderer":
items.Add(new PlaylistVideoItem
{
Id = rendererItem?["videoId"]?.ToString(),
Index = rendererItem?["index"]?["simpleText"]?.ToObject<long>() ?? 0,
Title = Utils.ReadRuns(rendererItem?["title"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
Channel = new Channel
{
Name = rendererItem?["shortBylineText"]?["runs"]?[0]?["text"]?.ToString(),
Id = rendererItem?["shortBylineText"]?["runs"]?[0]?["navigationEndpoint"]?[
"browseEndpoint"]?["browseId"]?.ToString(),
SubscriberCount = null,
Avatars = null
},
Duration = rendererItem?["lengthText"]?["simpleText"]?.ToString()
});
break;
case "itemSectionRenderer":
items.Add(new ItemSectionItem
{
Contents = ParseRenderers(rendererItem?["contents"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "gridRenderer":
items.Add(new ItemSectionItem
{
Contents = ParseRenderers(rendererItem?["items"]?.ToObject<JArray>() ?? new JArray())
});
break;
case "messageRenderer":
items.Add(new MessageItem
{
Title = rendererItem?["text"]?["simpleText"]?.ToString()
});
break;
case "channelAboutFullMetadataRenderer":
items.Add(new ChannelAboutItem
{
Description = rendererItem?["description"]?["simpleText"]?.ToString(),
Country = rendererItem?["country"]?["simpleText"]?.ToString(),
Joined = Utils.ReadRuns(rendererItem?["joinedDateText"]?["runs"]?.ToObject<JArray>() ??
new JArray()),
ViewCount = rendererItem?["viewCountText"]?["simpleText"]?.ToString()
});
break;
case "compactStationRenderer":
items.Add(new StationItem
{
Id = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["playlistId"]?.ToString(),
Title = rendererItem?["title"]?["simpleText"]?.ToString(),
Thumbnails =
(rendererItem?["thumbnail"]?["thumbnails"]?.ToObject<JArray>() ??
new JArray()).Select(Utils.ParseThumbnails).ToArray(),
VideoCount = rendererItem?["videoCountText"]?["runs"]?[0]?["text"].ToObject<int>() ?? 0,
FirstVideoId = rendererItem?["navigationEndpoint"]?["watchEndpoint"]?["videoId"]?.ToString(),
Description = rendererItem?["description"]?["simpleText"]?.ToString()
});
break;
case "metadataBadgeRenderer":
items.Add(new BadgeItem
{
Title = rendererItem?["label"]?.ToString(),
Style = rendererItem?["style"]?.ToString()
});
break;
case "promotedSparklesWebRenderer":
// this is an ad
// no one likes ads
break;
default:
items.Add(new DynamicItem
{
Id = rendererName,
Title = rendererItem?.ToString()
});
break;
}
}
return items.ToArray();
}
}
}

View File

@@ -0,0 +1,7 @@
namespace LightTube.Contexts
{
public class BaseContext
{
public bool MobileLayout;
}
}

View File

@@ -0,0 +1,7 @@
namespace LightTube.Contexts
{
public class ErrorContext : BaseContext
{
public string Path;
}
}

View File

@@ -0,0 +1,11 @@
using LightTube.Database;
namespace LightTube.Contexts
{
public class FeedContext : BaseContext
{
public LTChannel[] Channels;
public FeedVideo[] Videos;
public string RssToken;
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using InnerTube.Models;
namespace LightTube.Contexts
{
public class LocalsContext : BaseContext
{
public Dictionary<string, string> Languages;
public Dictionary<string, string> Regions;
public string CurrentLanguage;
public string CurrentRegion;
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using InnerTube.Models;
using LightTube.Database;
namespace LightTube.Contexts
{
public class PlaylistsContext : BaseContext
{
public IEnumerable<LTPlaylist> Playlists;
}
}

View File

@@ -0,0 +1,351 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using InnerTube;
using InnerTube.Models;
using LightTube.Contexts;
using LightTube.Database;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace LightTube.Controllers
{
public class AccountController : Controller
{
private readonly Youtube _youtube;
public AccountController(Youtube youtube)
{
_youtube = youtube;
}
[Route("/Account")]
public IActionResult Account()
{
return View(new BaseContext
{
MobileLayout = Utils.IsClientMobile(Request)
});
}
[HttpGet]
public IActionResult Login(string err = null)
{
if (HttpContext.TryGetUser(out LTUser _, "web"))
return Redirect("/");
return View(new MessageContext
{
Message = err,
MobileLayout = Utils.IsClientMobile(Request)
});
}
[HttpPost]
public async Task<IActionResult> Login(string userid, string password)
{
if (HttpContext.TryGetUser(out LTUser _, "web"))
return Redirect("/");
try
{
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
Response.Cookies.Append("token", login.Token, new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
return Redirect("/");
}
catch (KeyNotFoundException e)
{
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
}
catch (UnauthorizedAccessException e)
{
return Redirect("/Account/Login?err=" + HttpUtility.UrlEncode(e.Message));
}
}
public async Task<IActionResult> Logout()
{
if (HttpContext.Request.Cookies.TryGetValue("token", out string token))
{
await DatabaseManager.Logins.RemoveToken(token);
}
HttpContext.Response.Cookies.Delete("token");
HttpContext.Response.Cookies.Delete("account_data");
return Redirect("/");
}
[HttpGet]
public IActionResult Register(string err = null)
{
if (HttpContext.TryGetUser(out LTUser _, "web"))
return Redirect("/");
return View(new MessageContext
{
Message = err,
MobileLayout = Utils.IsClientMobile(Request)
});
}
[HttpPost]
public async Task<IActionResult> Register(string userid, string password)
{
if (HttpContext.TryGetUser(out LTUser _, "web"))
return Redirect("/");
try
{
await DatabaseManager.Logins.CreateUser(userid, password);
LTLogin login = await DatabaseManager.Logins.CreateToken(userid, password, Request.Headers["user-agent"], new []{"web"});
Response.Cookies.Append("token", login.Token, new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
return Redirect("/");
}
catch (DuplicateNameException e)
{
return Redirect("/Account/Register?err=" + HttpUtility.UrlEncode(e.Message));
}
}
public IActionResult RegisterLocal()
{
if (!HttpContext.TryGetUser(out LTUser _, "web"))
HttpContext.CreateLocalAccount();
return Redirect("/");
}
[HttpGet]
public IActionResult Delete(string err = null)
{
if (!HttpContext.TryGetUser(out LTUser _, "web"))
return Redirect("/");
return View(new MessageContext
{
Message = err,
MobileLayout = Utils.IsClientMobile(Request)
});
}
[HttpPost]
public async Task<IActionResult> Delete(string userid, string password)
{
try
{
if (userid == "Local Account" && password == "local_account")
Response.Cookies.Delete("account_data");
else
await DatabaseManager.Logins.DeleteUser(userid, password);
return Redirect("/Account/Register?err=Account+deleted");
}
catch (KeyNotFoundException e)
{
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
}
catch (UnauthorizedAccessException e)
{
return Redirect("/Account/Delete?err=" + HttpUtility.UrlEncode(e.Message));
}
}
public async Task<IActionResult> Logins()
{
if (!HttpContext.TryGetUser(out LTUser _, "web") || !HttpContext.Request.Cookies.TryGetValue("token", out string token))
return Redirect("/Account/Login");
return View(new LoginsContext
{
CurrentLogin = await DatabaseManager.Logins.GetCurrentLoginId(token),
Logins = await DatabaseManager.Logins.GetAllUserTokens(token),
MobileLayout = Utils.IsClientMobile(Request)
});
}
public async Task<IActionResult> DisableLogin(string id)
{
if (!HttpContext.Request.Cookies.TryGetValue("token", out string token))
return Redirect("/Account/Login");
try
{
await DatabaseManager.Logins.RemoveTokenFromId(token, id);
} catch { }
return Redirect("/Account/Logins");
}
public async Task<IActionResult> Subscribe(string channel)
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
return Unauthorized();
try
{
YoutubeChannel youtubeChannel = await _youtube.GetChannelAsync(channel, ChannelTabs.About);
(LTChannel channel, bool subscribed) result;
result.channel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
youtubeChannel.Avatars.First().Url);
if (user.PasswordHash == "local_account")
{
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(youtubeChannel.Id, youtubeChannel.Name, youtubeChannel.Subscribers,
youtubeChannel.Avatars.First().Url);
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
user.SubscribedChannels.Remove(ltChannel.ChannelId);
else
user.SubscribedChannels.Add(ltChannel.ChannelId);
HttpContext.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user),
new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
result.subscribed = user.SubscribedChannels.Contains(ltChannel.ChannelId);
}
else
{
result =
await DatabaseManager.Logins.SubscribeToChannel(user, youtubeChannel);
}
return Ok(result.subscribed ? "true" : "false");
}
catch
{
return Unauthorized();
}
}
public IActionResult SubscriptionsJson()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
return Json(Array.Empty<string>());
try
{
return Json(user.SubscribedChannels);
}
catch
{
return Json(Array.Empty<string>());
}
}
public async Task<IActionResult> Settings()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
Redirect("/Account/Login");
if (Request.Method == "POST")
{
CookieOptions opts = new()
{
Expires = DateTimeOffset.MaxValue
};
foreach ((string key, StringValues value) in Request.Form)
{
switch (key)
{
case "theme":
Response.Cookies.Append("theme", value, opts);
break;
case "hl":
Response.Cookies.Append("hl", value, opts);
break;
case "gl":
Response.Cookies.Append("gl", value, opts);
break;
case "compatibility":
Response.Cookies.Append("compatibility", value, opts);
break;
case "api-access":
await DatabaseManager.Logins.SetApiAccess(user, bool.Parse(value));
break;
}
}
return Redirect("/Account");
}
YoutubeLocals locals = await _youtube.GetLocalsAsync();
Request.Cookies.TryGetValue("theme", out string theme);
bool compatibility = false;
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out compatibility);
return View(new SettingsContext
{
Languages = locals.Languages,
Regions = locals.Regions,
CurrentLanguage = HttpContext.GetLanguage(),
CurrentRegion = HttpContext.GetRegion(),
MobileLayout = Utils.IsClientMobile(Request),
Theme = theme ?? "light",
CompatibilityMode = compatibility,
ApiAccess = user.ApiAccess
});
}
public async Task<IActionResult> AddVideoToPlaylist(string v)
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
Redirect("/Account/Login");
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(v, new HttpClient());
return View(new AddToPlaylistContext
{
Id = v,
Video = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID),
Thumbnail = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?[0]?["url"]?.ToString() ?? $"https://i.ytimg.com/vi_webp/{v}/maxresdefault.webp",
MobileLayout = Utils.IsClientMobile(Request),
});
}
[HttpGet]
public IActionResult CreatePlaylist(string returnUrl = null)
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
Redirect("/Account/Login");
return View(new BaseContext
{
MobileLayout = Utils.IsClientMobile(Request),
});
}
[HttpPost]
public async Task<IActionResult> CreatePlaylist()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
Redirect("/Account/Login");
if (!Request.Form.ContainsKey("name") || string.IsNullOrWhiteSpace(Request.Form["name"])) return BadRequest();
LTPlaylist pl = await DatabaseManager.Playlists.CreatePlaylist(
user,
Request.Form["name"],
string.IsNullOrWhiteSpace(Request.Form["description"]) ? "" : Request.Form["description"],
Enum.Parse<PlaylistVisibility>(string.IsNullOrWhiteSpace(Request.Form["visibility"]) ? "UNLISTED" : Request.Form["visibility"]));
return Redirect($"/playlist?list={pl.Id}");
}
}
}

View File

@@ -0,0 +1,187 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using InnerTube;
using InnerTube.Models;
using Microsoft.AspNetCore.Mvc;
namespace LightTube.Controllers
{
[Route("/api")]
public class ApiController : Controller
{
private const string VideoIdRegex = @"[a-zA-Z0-9_-]{11}";
private const string ChannelIdRegex = @"[a-zA-Z0-9_-]{24}";
private const string PlaylistIdRegex = @"[a-zA-Z0-9_-]{34}";
private readonly Youtube _youtube;
public ApiController(Youtube youtube)
{
_youtube = youtube;
}
private IActionResult Xml(XmlNode xmlDocument)
{
MemoryStream ms = new();
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
ms.Position = 0;
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
return File(ms, "application/xml");
}
[Route("player")]
public async Task<IActionResult> GetPlayerInfo(string v)
{
if (v is null)
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
Regex regex = new(VideoIdRegex);
if (!regex.IsMatch(v) || v.Length != 11)
return GetErrorVideoPlayer(v, "Invalid YouTube ID " + v);
try
{
YoutubePlayer player =
await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
catch (Exception e)
{
return GetErrorVideoPlayer(v, e.Message);
}
}
private IActionResult GetErrorVideoPlayer(string videoId, string message)
{
YoutubePlayer player = new()
{
Id = videoId,
Title = "",
Description = "",
Tags = Array.Empty<string>(),
Channel = new Channel
{
Name = "",
Id = "",
Avatars = Array.Empty<Thumbnail>()
},
Duration = 0,
Chapters = Array.Empty<Chapter>(),
Thumbnails = Array.Empty<Thumbnail>(),
Formats = Array.Empty<Format>(),
AdaptiveFormats = Array.Empty<Format>(),
Subtitles = Array.Empty<Subtitle>(),
Storyboards = Array.Empty<string>(),
ExpiresInSeconds = "0",
ErrorMessage = message
};
return Xml(player.GetXmlDocument());
}
[Route("video")]
public async Task<IActionResult> GetVideoInfo(string v)
{
if (v is null)
return GetErrorVideoPlayer("", "Missing YouTube ID (query parameter `v`)");
Regex regex = new(VideoIdRegex);
if (!regex.IsMatch(v) || v.Length != 11)
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Error");
item.InnerText = "Invalid YouTube ID " + v;
doc.AppendChild(item);
return Xml(doc);
}
YoutubeVideo player = await _youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
[Route("search")]
public async Task<IActionResult> Search(string query, string continuation = null)
{
if (string.IsNullOrWhiteSpace(query) && string.IsNullOrWhiteSpace(continuation))
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Error");
item.InnerText = "Invalid query " + query;
doc.AppendChild(item);
return Xml(doc);
}
YoutubeSearchResults player = await _youtube.SearchAsync(query, continuation, HttpContext.GetLanguage(),
HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
[Route("playlist")]
public async Task<IActionResult> Playlist(string id, string continuation = null)
{
Regex regex = new(PlaylistIdRegex);
if (!regex.IsMatch(id) || id.Length != 34) return GetErrorVideoPlayer(id, "Invalid playlist ID " + id);
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Error");
item.InnerText = "Invalid ID " + id;
doc.AppendChild(item);
return Xml(doc);
}
YoutubePlaylist player = await _youtube.GetPlaylistAsync(id, continuation, HttpContext.GetLanguage(),
HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
[Route("channel")]
public async Task<IActionResult> Channel(string id, ChannelTabs tab = ChannelTabs.Home,
string continuation = null)
{
Regex regex = new(ChannelIdRegex);
if (!regex.IsMatch(id) || id.Length != 24) return GetErrorVideoPlayer(id, "Invalid channel ID " + id);
if (string.IsNullOrWhiteSpace(id) && string.IsNullOrWhiteSpace(continuation))
{
XmlDocument doc = new();
XmlElement item = doc.CreateElement("Error");
item.InnerText = "Invalid ID " + id;
doc.AppendChild(item);
return Xml(doc);
}
YoutubeChannel player = await _youtube.GetChannelAsync(id, tab, continuation, HttpContext.GetLanguage(),
HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
[Route("trending")]
public async Task<IActionResult> Trending(string id, string continuation = null)
{
YoutubeTrends player = await _youtube.GetExploreAsync(id, continuation,
HttpContext.GetLanguage(),
HttpContext.GetRegion());
XmlDocument xml = player.GetXmlDocument();
return Xml(xml);
}
}
}

View File

@@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using InnerTube;
using InnerTube.Models;
using LightTube.Database;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
namespace LightTube.Controllers
{
[Route("/api/auth")]
public class AuthorizedApiController : Controller
{
private readonly Youtube _youtube;
private IReadOnlyList<string> _scopes = new[]
{
"api.subscriptions.read",
"api.subscriptions.write"
};
public AuthorizedApiController(Youtube youtube)
{
_youtube = youtube;
}
private IActionResult Xml(XmlNode xmlDocument, HttpStatusCode statusCode)
{
MemoryStream ms = new();
ms.Write(Encoding.UTF8.GetBytes(xmlDocument.OuterXml));
ms.Position = 0;
HttpContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");
Response.StatusCode = (int)statusCode;
return File(ms, "application/xml");
}
private XmlNode BuildErrorXml(string message)
{
XmlDocument doc = new();
XmlElement error = doc.CreateElement("Error");
error.InnerText = message;
doc.AppendChild(error);
return doc;
}
[HttpPost]
[Route("getToken")]
public async Task<IActionResult> GetToken()
{
if (!Request.Headers.TryGetValue("User-Agent", out StringValues userAgent))
return Xml(BuildErrorXml("Missing User-Agent header"), HttpStatusCode.BadRequest);
Match match = Regex.Match(userAgent.ToString(), DatabaseManager.ApiUaRegex);
if (!match.Success)
return Xml(BuildErrorXml("Bad User-Agent header. Please see 'Documentation/API requests'"), HttpStatusCode.BadRequest);
if (match.Groups[1].ToString() != "1.0")
return Xml(BuildErrorXml($"Unknown API version {match.Groups[1]}"), HttpStatusCode.BadRequest);
if (!Request.Form.TryGetValue("user", out StringValues user))
return Xml(BuildErrorXml("Missing request value: 'user'"), HttpStatusCode.BadRequest);
if (!Request.Form.TryGetValue("password", out StringValues password))
return Xml(BuildErrorXml("Missing request value: 'password'"), HttpStatusCode.BadRequest);
if (!Request.Form.TryGetValue("scopes", out StringValues scopes))
return Xml(BuildErrorXml("Missing request value: 'scopes'"), HttpStatusCode.BadRequest);
string[] newScopes = scopes.First().Split(",");
foreach (string s in newScopes)
if (!_scopes.Contains(s))
return Xml(BuildErrorXml($"Unknown scope '{s}'"), HttpStatusCode.BadRequest);
try
{
LTLogin ltLogin =
await DatabaseManager.Logins.CreateToken(user, password, userAgent.ToString(),
scopes.First().Split(","));
return Xml(ltLogin.GetXmlElement(), HttpStatusCode.Created);
}
catch (UnauthorizedAccessException)
{
return Xml(BuildErrorXml("Invalid credentials"), HttpStatusCode.Unauthorized);
}
catch (InvalidOperationException)
{
return Xml(BuildErrorXml("User has API access disabled"), HttpStatusCode.Forbidden);
}
}
[Route("subscriptions/feed")]
public async Task<IActionResult> SubscriptionsFeed()
{
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
SubscriptionFeed feed = new()
{
videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels)
};
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
}
[HttpGet]
[Route("subscriptions/channels")]
public IActionResult SubscriptionsChannels()
{
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.read"))
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
SubscriptionChannels feed = new()
{
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray()
};
Array.Sort(feed.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
return Xml(feed.GetXmlDocument(), HttpStatusCode.OK);
}
[HttpPut]
[Route("subscriptions/channels")]
public async Task<IActionResult> Subscribe()
{
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
Request.Form.TryGetValue("id", out StringValues ids);
string id = ids.ToString();
if (user.SubscribedChannels.Contains(id))
return StatusCode((int)HttpStatusCode.NotModified);
try
{
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
if (channel.Id is null)
return StatusCode((int)HttpStatusCode.NotFound);
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
XmlDocument doc = new();
doc.AppendChild(ltChannel.GetXmlElement(doc));
return Xml(doc, HttpStatusCode.OK);
}
catch (Exception e)
{
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
}
}
[HttpDelete]
[Route("subscriptions/channels")]
public async Task<IActionResult> Unsubscribe()
{
if (!HttpContext.TryGetUser(out LTUser user, "api.subscriptions.write"))
return Xml(BuildErrorXml("Unauthorized"), HttpStatusCode.Unauthorized);
Request.Form.TryGetValue("id", out StringValues ids);
string id = ids.ToString();
if (!user.SubscribedChannels.Contains(id))
return StatusCode((int)HttpStatusCode.NotModified);
try
{
YoutubeChannel channel = await _youtube.GetChannelAsync(id);
if (channel.Id is null)
return StatusCode((int)HttpStatusCode.NotFound);
(LTChannel ltChannel, bool _) = await DatabaseManager.Logins.SubscribeToChannel(user, channel);
XmlDocument doc = new();
doc.AppendChild(ltChannel.GetXmlElement(doc));
return Xml(doc, HttpStatusCode.OK);
}
catch (Exception e)
{
return Xml(BuildErrorXml(e.Message), HttpStatusCode.InternalServerError);
}
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LightTube.Contexts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using InnerTube;
using LightTube.Database;
namespace LightTube.Controllers
{
[Route("/feed")]
public class FeedController : Controller
{
private readonly ILogger<FeedController> _logger;
private readonly Youtube _youtube;
public FeedController(ILogger<FeedController> logger, Youtube youtube)
{
_logger = logger;
_youtube = youtube;
}
[Route("subscriptions")]
public async Task<IActionResult> Subscriptions()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
return Redirect("/Account/Login");
try
{
FeedContext context = new()
{
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
Videos = await YoutubeRSS.GetMultipleFeeds(user.SubscribedChannels),
RssToken = user.RssToken,
MobileLayout = Utils.IsClientMobile(Request)
};
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
return View(context);
}
catch
{
HttpContext.Response.Cookies.Delete("token");
return Redirect("/Account/Login");
}
}
[Route("channels")]
public IActionResult Channels()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
return Redirect("/Account/Login");
try
{
FeedContext context = new()
{
Channels = user.SubscribedChannels.Select(DatabaseManager.Channels.GetChannel).ToArray(),
Videos = null,
MobileLayout = Utils.IsClientMobile(Request)
};
Array.Sort(context.Channels, (p, q) => string.Compare(p.Name, q.Name, StringComparison.OrdinalIgnoreCase));
return View(context);
}
catch
{
HttpContext.Response.Cookies.Delete("token");
return Redirect("/Account/Login");
}
}
[Route("explore")]
public IActionResult Explore()
{
return View(new BaseContext
{
MobileLayout = Utils.IsClientMobile(Request)
});
}
[Route("/feed/library")]
public async Task<IActionResult> Playlists()
{
if (!HttpContext.TryGetUser(out LTUser user, "web"))
Redirect("/Account/Login");
return View(new PlaylistsContext
{
MobileLayout = Utils.IsClientMobile(Request),
Playlists = await DatabaseManager.Playlists.GetUserPlaylists(user.UserID)
});
}
[Route("/rss")]
public async Task<IActionResult> Playlists(string token, int limit = 15)
{
if (!DatabaseManager.TryGetRssUser(token, out LTUser user))
return Unauthorized();
return File(Encoding.UTF8.GetBytes(await user.GenerateRssFeed(Request.Host.ToString(), Math.Clamp(limit, 0, 50))), "application/xml");
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using LightTube.Contexts;
using LightTube.Models;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using InnerTube;
using InnerTube.Models;
using ErrorContext = LightTube.Contexts.ErrorContext;
namespace LightTube.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly Youtube _youtube;
public HomeController(ILogger<HomeController> logger, Youtube youtube)
{
_logger = logger;
_youtube = youtube;
}
public IActionResult Index()
{
return View(new BaseContext
{
MobileLayout = Utils.IsClientMobile(Request)
});
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorContext
{
Path = HttpContext.Features.Get<IExceptionHandlerPathFeature>().Path,
MobileLayout = Utils.IsClientMobile(Request)
});
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using InnerTube;
using InnerTube.Models;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
namespace LightTube.Controllers
{
[Route("/manifest")]
public class ManifestController : Controller
{
private readonly Youtube _youtube;
private readonly HttpClient _client = new();
public ManifestController(Youtube youtube)
{
_youtube = youtube;
}
[Route("{v}")]
public async Task<IActionResult> DefaultManifest(string v)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
return StatusCode(500, player.ErrorMessage);
return Redirect(player.IsLive ? $"/manifest/{v}.m3u8" : $"/manifest/{v}.mpd" + Request.QueryString);
}
[Route("{v}.mpd")]
public async Task<IActionResult> DashManifest(string v, string videoCodec = null, string audioCodec = null, bool useProxy = true)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion());
string manifest = player.GetMpdManifest(useProxy ? $"https://{Request.Host}/proxy/" : null, videoCodec, audioCodec);
return File(Encoding.UTF8.GetBytes(manifest), "application/dash+xml");
}
[Route("{v}.m3u8")]
public async Task<IActionResult> HlsManifest(string v, bool useProxy = true)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion(), true);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
return StatusCode(403, player.ErrorMessage);
if (player.IsLive)
{
string manifest = await player.GetHlsManifest(useProxy ? $"https://{Request.Host}/proxy" : null);
return File(Encoding.UTF8.GetBytes(manifest), "application/vnd.apple.mpegurl");
}
if (useProxy)
return StatusCode(400, "HLS proxy for non-live videos are not supported at the moment.");
return Redirect(player.HlsManifestUrl);
}
}
}

View File

@@ -0,0 +1,517 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using InnerTube;
using InnerTube.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace LightTube.Controllers
{
[Route("/proxy")]
public class ProxyController : Controller
{
private readonly ILogger<YoutubeController> _logger;
private readonly Youtube _youtube;
private string[] BlockedHeaders =
{
"host",
"cookies"
};
public ProxyController(ILogger<YoutubeController> logger, Youtube youtube)
{
_logger = logger;
_youtube = youtube;
}
[Route("media/{videoId}/{formatId}")]
public async Task Media(string videoId, string formatId)
{
try
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
await Response.StartAsync();
return;
}
List<Format> formats = new();
formats.AddRange(player.Formats);
formats.AddRange(player.AdaptiveFormats);
if (!formats.Any(x => x.FormatId == formatId))
{
Response.StatusCode = (int) HttpStatusCode.NotFound;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
await Response.StartAsync();
return;
}
string url = formats.First(x => x.FormatId == formatId).Url;
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.Method = Request.Method;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
HttpWebResponse response;
try
{
response = (HttpWebResponse) request.GetResponse();
}
catch (WebException e)
{
response = e.Response as HttpWebResponse;
}
if (response == null)
await Response.StartAsync();
foreach (string header in response.Headers.AllKeys)
if (Response.Headers.ContainsKey(header))
Response.Headers[header] = response.Headers.Get(header);
else
Response.Headers.Add(header, response.Headers.Get(header));
Response.StatusCode = (int) response.StatusCode;
await using Stream stream = response.GetResponseStream();
try
{
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
}
catch (Exception)
{
// an exception is thrown if the client suddenly stops streaming
}
await Response.StartAsync();
}
catch (Exception e)
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
await Response.StartAsync();
}
}
[Route("download/{videoId}/{formatId}/{filename}")]
public async Task Download(string videoId, string formatId, string filename)
{
try
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
await Response.StartAsync();
return;
}
List<Format> formats = new();
formats.AddRange(player.Formats);
formats.AddRange(player.AdaptiveFormats);
if (!formats.Any(x => x.FormatId == formatId))
{
Response.StatusCode = (int) HttpStatusCode.NotFound;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
$"Format with ID {formatId} not found.\nAvailable IDs are: {string.Join(", ", formats.Select(x => x.FormatId.ToString()))}"));
await Response.StartAsync();
return;
}
string url = formats.First(x => x.FormatId == formatId).Url;
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.Method = Request.Method;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
HttpWebResponse response;
try
{
response = (HttpWebResponse) request.GetResponse();
}
catch (WebException e)
{
response = e.Response as HttpWebResponse;
}
if (response == null)
await Response.StartAsync();
foreach (string header in response.Headers.AllKeys)
if (Response.Headers.ContainsKey(header))
Response.Headers[header] = response.Headers.Get(header);
else
Response.Headers.Add(header, response.Headers.Get(header));
Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{Regex.Replace(filename, @"[^\u0000-\u007F]+", string.Empty)}\"");
Response.StatusCode = (int) response.StatusCode;
await using Stream stream = response.GetResponseStream();
try
{
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
}
catch (Exception)
{
// an exception is thrown if the client suddenly stops streaming
}
await Response.StartAsync();
}
catch (Exception e)
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
await Response.StartAsync();
}
}
[Route("caption/{videoId}/{language}")]
public async Task<FileStreamResult> SubtitleProxy(string videoId, string language)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
"text/plain");
}
string url = null;
Subtitle? subtitle = player.Subtitles.FirstOrDefault(x => string.Equals(x.Language, language, StringComparison.InvariantCultureIgnoreCase));
if (subtitle is null)
{
Response.StatusCode = (int) HttpStatusCode.NotFound;
return File(
new MemoryStream(Encoding.UTF8.GetBytes(
$"There are no available subtitles for {language}. Available language codes are: {string.Join(", ", player.Subtitles.Select(x => $"\"{x.Language}\""))}")),
"text/plain");
}
url = subtitle.Url.Replace("fmt=srv3", "fmt=vtt");
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
await using Stream stream = response.GetResponseStream();
using StreamReader reader = new(stream);
return File(new MemoryStream(Encoding.UTF8.GetBytes(await reader.ReadToEndAsync())),
"text/vtt");
}
[Route("image")]
[Obsolete("Use /proxy/thumbnail instead")]
public async Task ImageProxy(string url)
{
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
foreach (string header in response.Headers.AllKeys)
if (Response.Headers.ContainsKey(header))
Response.Headers[header] = response.Headers.Get(header);
else
Response.Headers.Add(header, response.Headers.Get(header));
Response.StatusCode = (int)response.StatusCode;
await using Stream stream = response.GetResponseStream();
await stream.CopyToAsync(Response.Body);
await Response.StartAsync();
}
[Route("thumbnail/{videoId}/{index:int}")]
public async Task ThumbnailProxy(string videoId, int index = 0)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
if (index == -1) index = player.Thumbnails.Length - 1;
if (index >= player.Thumbnails.Length)
{
Response.StatusCode = 404;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(
$"Cannot find thumbnail #{index} for {videoId}. The maximum quality is {player.Thumbnails.Length - 1}"));
await Response.StartAsync();
return;
}
string url = player.Thumbnails.FirstOrDefault()?.Url;
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
foreach (string header in response.Headers.AllKeys)
if (Response.Headers.ContainsKey(header))
Response.Headers[header] = response.Headers.Get(header);
else
Response.Headers.Add(header, response.Headers.Get(header));
Response.StatusCode = (int)response.StatusCode;
await using Stream stream = response.GetResponseStream();
await stream.CopyToAsync(Response.Body);
await Response.StartAsync();
}
[Route("storyboard/{videoId}")]
public async Task StoryboardProxy(string videoId)
{
try
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(player.ErrorMessage));
await Response.StartAsync();
return;
}
if (!player.Storyboards.Any())
{
Response.StatusCode = (int) HttpStatusCode.NotFound;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes("No usable storyboard found."));
await Response.StartAsync();
return;
}
string url = player.Storyboards.First();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
foreach (string header in response.Headers.AllKeys)
if (Response.Headers.ContainsKey(header))
Response.Headers[header] = response.Headers.Get(header);
else
Response.Headers.Add(header, response.Headers.Get(header));
Response.StatusCode = (int)response.StatusCode;
await using Stream stream = response.GetResponseStream();
await stream.CopyToAsync(Response.Body);
await Response.StartAsync();
}
catch (Exception e)
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
await Response.Body.WriteAsync(Encoding.UTF8.GetBytes(e.ToString()));
await Response.StartAsync();
}
}
[Route("hls")]
public async Task<IActionResult> HlsProxy(string url)
{
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
await using Stream stream = response.GetResponseStream();
using StreamReader reader = new(stream);
string manifest = await reader.ReadToEndAsync();
StringBuilder proxyManifest = new ();
foreach (string s in manifest.Split("\n"))
{
// also check if proxy enabled
proxyManifest.AppendLine(!s.StartsWith("http")
? s
: $"https://{Request.Host}/proxy/video?url={HttpUtility.UrlEncode(s)}");
}
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
"application/vnd.apple.mpegurl");
}
[Route("manifest/{videoId}")]
public async Task<IActionResult> ManifestProxy(string videoId, string formatId, bool useProxy = true)
{
YoutubePlayer player = await _youtube.GetPlayerAsync(videoId, iOS: true);
if (!string.IsNullOrWhiteSpace(player.ErrorMessage))
{
Response.StatusCode = (int) HttpStatusCode.InternalServerError;
return File(new MemoryStream(Encoding.UTF8.GetBytes(player.ErrorMessage)),
"text/plain");
}
if (player.HlsManifestUrl == null)
{
Response.StatusCode = (int) HttpStatusCode.NotFound;
return File(new MemoryStream(Encoding.UTF8.GetBytes("This video does not have an HLS manifest URL")),
"text/plain");
}
string url = player.HlsManifestUrl;
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
await using Stream stream = response.GetResponseStream();
using StreamReader reader = new(stream);
string manifest = await reader.ReadToEndAsync();
StringBuilder proxyManifest = new ();
if (useProxy)
foreach (string s in manifest.Split("\n"))
{
// also check if proxy enabled
proxyManifest.AppendLine(!s.StartsWith("http")
? s
: $"https://{Request.Host}/proxy/ytmanifest?path=" + HttpUtility.UrlEncode(s[46..]));
}
else
proxyManifest.Append(manifest);
return File(new MemoryStream(Encoding.UTF8.GetBytes(proxyManifest.ToString())),
"application/vnd.apple.mpegurl");
}
[Route("ytmanifest")]
public async Task<IActionResult> YoutubeManifestProxy(string path)
{
string url = "https://manifest.googlevideo.com" + path;
StringBuilder sb = new();
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
await using Stream stream = response.GetResponseStream();
using StreamReader reader = new(stream);
string manifest = await reader.ReadToEndAsync();
foreach (string line in manifest.Split("\n"))
{
if (string.IsNullOrWhiteSpace(line))
sb.AppendLine();
else if (line.StartsWith("#"))
sb.AppendLine(line);
else
{
Uri u = new(line);
sb.AppendLine($"https://{Request.Host}/proxy/videoplayback?host={u.Host}&path={HttpUtility.UrlEncode(u.PathAndQuery)}");
}
}
return File(new MemoryStream(Encoding.UTF8.GetBytes(sb.ToString())),
"application/vnd.apple.mpegurl");
}
[Route("videoplayback")]
public async Task VideoPlaybackProxy(string path, string host)
{
// make sure this is only used in livestreams
string url = $"https://{host}{path}";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
foreach ((string header, StringValues values) in HttpContext.Request.Headers.Where(header =>
!header.Key.StartsWith(":") && !BlockedHeaders.Contains(header.Key.ToLower())))
foreach (string value in values)
request.Headers.Add(header, value);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
await using Stream stream = response.GetResponseStream();
Response.ContentType = "application/octet-stream";
await Response.StartAsync();
await stream.CopyToAsync(Response.Body, HttpContext.RequestAborted);
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace LightTube.Controllers
{
[Route("/toggles")]
public class TogglesController : Controller
{
[Route("theme")]
public IActionResult ToggleTheme(string redirectUrl)
{
if (Request.Cookies.TryGetValue("theme", out string theme))
Response.Cookies.Append("theme", theme switch
{
"light" => "dark",
"dark" => "light",
var _ => "dark"
}, new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
else
Response.Cookies.Append("theme", "light");
return Redirect(redirectUrl);
}
[Route("compatibility")]
public IActionResult ToggleCompatibility(string redirectUrl)
{
if (Request.Cookies.TryGetValue("compatibility", out string compatibility))
Response.Cookies.Append("compatibility", compatibility switch
{
"true" => "false",
"false" => "true",
var _ => "true"
}, new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
else
Response.Cookies.Append("compatibility", "true");
return Redirect(redirectUrl);
}
[Route("collapse_guide")]
public IActionResult ToggleCollapseGuide(string redirectUrl)
{
if (Request.Cookies.TryGetValue("minmode", out string minmode))
Response.Cookies.Append("minmode", minmode switch
{
"true" => "false",
"false" => "true",
var _ => "true"
}, new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
else
Response.Cookies.Append("minmode", "true");
return Redirect(redirectUrl);
}
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using LightTube.Contexts;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using InnerTube;
using InnerTube.Models;
using LightTube.Database;
namespace LightTube.Controllers
{
public class YoutubeController : Controller
{
private readonly ILogger<YoutubeController> _logger;
private readonly Youtube _youtube;
public YoutubeController(ILogger<YoutubeController> logger, Youtube youtube)
{
_logger = logger;
_youtube = youtube;
}
[Route("/watch")]
public async Task<IActionResult> Watch(string v, string quality = null)
{
Task[] tasks = {
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
ReturnYouTubeDislike.GetDislikes(v)
};
await Task.WhenAll(tasks);
bool cookieCompatibility = false;
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out cookieCompatibility);
PlayerContext context = new()
{
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
MobileLayout = Utils.IsClientMobile(Request),
CompatibilityMode = cookieCompatibility
};
return View(context);
}
[Route("/download")]
public async Task<IActionResult> Download(string v)
{
Task[] tasks = {
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
ReturnYouTubeDislike.GetDislikes(v)
};
await Task.WhenAll(tasks);
bool cookieCompatibility = false;
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out cookieCompatibility);
PlayerContext context = new()
{
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
Engagement = null,
MobileLayout = Utils.IsClientMobile(Request),
CompatibilityMode = cookieCompatibility
};
return View(context);
}
[Route("/embed/{v}")]
public async Task<IActionResult> Embed(string v, string quality = null, bool compatibility = false)
{
Task[] tasks = {
_youtube.GetPlayerAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
_youtube.GetVideoAsync(v, HttpContext.GetLanguage(), HttpContext.GetRegion()),
ReturnYouTubeDislike.GetDislikes(v)
};
try
{
await Task.WhenAll(tasks);
}
catch { }
bool cookieCompatibility = false;
if (Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out cookieCompatibility);
PlayerContext context = new()
{
Player = (tasks[0] as Task<YoutubePlayer>)?.Result,
Video = (tasks[1] as Task<YoutubeVideo>)?.Result,
Engagement = (tasks[2] as Task<YoutubeDislikes>)?.Result,
Resolution = quality ?? (tasks[0] as Task<YoutubePlayer>)?.Result.Formats.FirstOrDefault(x => x.FormatId != "17")?.FormatNote,
CompatibilityMode = compatibility || cookieCompatibility,
MobileLayout = Utils.IsClientMobile(Request)
};
return View(context);
}
[Route("/results")]
public async Task<IActionResult> Search(string search_query, string continuation = null)
{
SearchContext context = new()
{
Query = search_query,
ContinuationKey = continuation,
MobileLayout = Utils.IsClientMobile(Request)
};
if (!string.IsNullOrWhiteSpace(search_query))
{
context.Results = await _youtube.SearchAsync(search_query, continuation, HttpContext.GetLanguage(),
HttpContext.GetRegion());
Response.Cookies.Append("search_query", search_query);
}
else
{
context.Results =
new YoutubeSearchResults
{
Refinements = Array.Empty<string>(),
EstimatedResults = 0,
Results = Array.Empty<DynamicItem>(),
ContinuationKey = null
};
}
return View(context);
}
[Route("/playlist")]
public async Task<IActionResult> Playlist(string list, string continuation = null, int? delete = null, string add = null, string remove = null)
{
HttpContext.TryGetUser(out LTUser user, "web");
YoutubePlaylist pl = list.StartsWith("LT-PL")
? await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist()
: await _youtube.GetPlaylistAsync(list, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion());
string message = "";
if (list.StartsWith("LT-PL") && (await DatabaseManager.Playlists.GetPlaylist(list)).Visibility == PlaylistVisibility.PRIVATE && pl.Channel.Name != user?.UserID)
pl = new YoutubePlaylist
{
Id = null,
Title = "",
Description = "",
VideoCount = "",
ViewCount = "",
LastUpdated = "",
Thumbnail = Array.Empty<Thumbnail>(),
Channel = new Channel
{
Name = "",
Id = "",
SubscriberCount = "",
Avatars = Array.Empty<Thumbnail>()
},
Videos = Array.Empty<DynamicItem>(),
ContinuationKey = null
};
if (string.IsNullOrWhiteSpace(pl.Title)) message = "Playlist unavailable";
if (list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID)
{
if (delete != null)
{
LTVideo removed = await DatabaseManager.Playlists.RemoveVideoFromPlaylist(list, delete.Value);
message += $"Removed video '{removed.Title}'";
}
if (add != null)
{
LTVideo added = await DatabaseManager.Playlists.AddVideoToPlaylist(list, add);
message += $"Added video '{added.Title}'";
}
if (!string.IsNullOrWhiteSpace(remove))
{
await DatabaseManager.Playlists.DeletePlaylist(list);
message = "Playlist deleted";
}
pl = await (await DatabaseManager.Playlists.GetPlaylist(list)).ToYoutubePlaylist();
}
PlaylistContext context = new()
{
Playlist = pl,
Id = list,
ContinuationToken = continuation,
MobileLayout = Utils.IsClientMobile(Request),
Message = message,
Editable = list.StartsWith("LT-PL") && pl.Channel.Name == user?.UserID
};
return View(context);
}
[Route("/channel/{id}")]
public async Task<IActionResult> Channel(string id, string continuation = null)
{
ChannelContext context = new()
{
Channel = await _youtube.GetChannelAsync(id, ChannelTabs.Videos, continuation, HttpContext.GetLanguage(), HttpContext.GetRegion()),
Id = id,
ContinuationToken = continuation,
MobileLayout = Utils.IsClientMobile(Request)
};
await DatabaseManager.Channels.UpdateChannel(context.Channel.Id, context.Channel.Name, context.Channel.Subscribers,
context.Channel.Avatars.First().Url.ToString());
return View(context);
}
[Route("/shorts/{id}")]
public IActionResult Shorts(string id)
{
// yea no fuck shorts
return Redirect("/watch?v=" + id);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Threading.Tasks;
using MongoDB.Driver;
namespace LightTube.Database
{
public class ChannelManager
{
private static IMongoCollection<LTChannel> _channelCacheCollection;
public ChannelManager(IMongoCollection<LTChannel> channelCacheCollection)
{
_channelCacheCollection = channelCacheCollection;
}
public LTChannel GetChannel(string id)
{
LTChannel res = _channelCacheCollection.FindSync(x => x.ChannelId == id).FirstOrDefault();
return res ?? new LTChannel
{
Name = "Unknown Channel",
ChannelId = id,
IconUrl = "",
Subscribers = ""
};
}
public async Task<LTChannel> UpdateChannel(string id, string name, string subscribers, string iconUrl)
{
LTChannel channel = new()
{
ChannelId = id,
Name = name,
Subscribers = subscribers,
IconUrl = iconUrl
};
if (channel.IconUrl is null && !string.IsNullOrWhiteSpace(GetChannel(id).IconUrl))
channel.IconUrl = GetChannel(id).IconUrl;
if (await _channelCacheCollection.CountDocumentsAsync(x => x.ChannelId == id) > 0)
await _channelCacheCollection.ReplaceOneAsync(x => x.ChannelId == id, channel);
else
await _channelCacheCollection.InsertOneAsync(channel);
return channel;
}
}
}

View File

@@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
using System.Linq;
using InnerTube;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using MongoDB.Driver;
using Newtonsoft.Json;
namespace LightTube.Database
{
public static class DatabaseManager
{
public static readonly string ApiUaRegex = "LightTubeApiClient\\/([0-9.]*) ([\\S]+?)\\/([0-9.]*) \\(([\\s\\S]+?)\\)";
private static IMongoCollection<LTUser> _userCollection;
private static IMongoCollection<LTLogin> _tokenCollection;
private static IMongoCollection<LTChannel> _channelCacheCollection;
private static IMongoCollection<LTPlaylist> _playlistCollection;
private static IMongoCollection<LTVideo> _videoCacheCollection;
public static LoginManager Logins { get; private set; }
public static ChannelManager Channels { get; private set; }
public static PlaylistManager Playlists { get; private set; }
public static void Init(string connstr, Youtube youtube)
{
MongoClient client = new(connstr);
IMongoDatabase database = client.GetDatabase("lighttube");
_userCollection = database.GetCollection<LTUser>("users");
_tokenCollection = database.GetCollection<LTLogin>("tokens");
_playlistCollection = database.GetCollection<LTPlaylist>("playlists");
_channelCacheCollection = database.GetCollection<LTChannel>("channelCache");
_videoCacheCollection = database.GetCollection<LTVideo>("videoCache");
Logins = new LoginManager(_userCollection, _tokenCollection);
Channels = new ChannelManager(_channelCacheCollection);
Playlists = new PlaylistManager(_userCollection, _playlistCollection, _videoCacheCollection, youtube);
}
public static void CreateLocalAccount(this HttpContext context)
{
bool accountExists = false;
// Check local account
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
{
try
{
if (accountJson != null)
{
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
accountExists = true;
}
}
catch { }
}
// Account already exists, just leave it there
if (accountExists) return;
LTUser user = new()
{
UserID = "Local Account",
PasswordHash = "local_account",
SubscribedChannels = new List<string>()
};
context.Response.Cookies.Append("account_data", JsonConvert.SerializeObject(user), new CookieOptions
{
Expires = DateTimeOffset.MaxValue
});
}
public static bool TryGetUser(this HttpContext context, out LTUser user, string requiredScope)
{
// Check local account
if (context.Request.Cookies.TryGetValue("account_data", out string accountJson))
{
try
{
if (accountJson != null)
{
LTUser tempUser = JsonConvert.DeserializeObject<LTUser>(accountJson) ?? new LTUser();
if (tempUser.UserID == "Local Account" && tempUser.PasswordHash == "local_account")
{
user = tempUser;
return true;
}
}
}
catch
{
user = null;
return false;
}
}
// Check cloud account
if (!context.Request.Cookies.TryGetValue("token", out string token))
if (context.Request.Headers.TryGetValue("Authorization", out StringValues tokens))
token = tokens.ToString();
else
{
user = null;
return false;
}
try
{
if (token != null)
{
user = Logins.GetUserFromToken(token).Result;
LTLogin login = Logins.GetLoginFromToken(token).Result;
if (login.Scopes.Contains(requiredScope))
{
#pragma warning disable 4014
login.UpdateLastAccess(DateTimeOffset.Now);
#pragma warning restore 4014
return true;
}
return false;
}
}
catch
{
user = null;
return false;
}
user = null;
return false;
}
public static bool TryGetRssUser(string token, out LTUser user)
{
if (token is null)
{
user = null;
return false;
}
try
{
user = Logins.GetUserFromRssToken(token).Result;
return true;
}
catch
{
user = null;
return false;
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Xml;
using MongoDB.Bson.Serialization.Attributes;
namespace LightTube.Database
{
[BsonIgnoreExtraElements]
public class LTChannel
{
public string ChannelId;
public string Name;
public string Subscribers;
public string IconUrl;
public XmlNode GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Channel");
item.SetAttribute("id", ChannelId);
item.SetAttribute("subscribers", Subscribers);
XmlElement title = doc.CreateElement("Name");
title.InnerText = Name;
item.AppendChild(title);
XmlElement thumbnail = doc.CreateElement("Avatar");
thumbnail.InnerText = IconUrl;
item.AppendChild(thumbnail);
return item;
}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using Humanizer;
using MongoDB.Bson.Serialization.Attributes;
using MyCSharp.HttpUserAgentParser;
namespace LightTube.Database
{
[BsonIgnoreExtraElements]
public class LTLogin
{
public string Identifier;
public string Email;
public string Token;
public string UserAgent;
public string[] Scopes;
public DateTimeOffset Created = DateTimeOffset.MinValue;
public DateTimeOffset LastSeen = DateTimeOffset.MinValue;
public XmlDocument GetXmlElement()
{
XmlDocument doc = new();
XmlElement login = doc.CreateElement("Login");
login.SetAttribute("id", Identifier);
login.SetAttribute("user", Email);
XmlElement token = doc.CreateElement("Token");
token.InnerText = Token;
login.AppendChild(token);
XmlElement scopes = doc.CreateElement("Scopes");
foreach (string scope in Scopes)
{
XmlElement scopeElement = doc.CreateElement("Scope");
scopeElement.InnerText = scope;
login.AppendChild(scopeElement);
}
login.AppendChild(scopes);
doc.AppendChild(login);
return doc;
}
public string GetTitle()
{
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
if (match.Success)
return $"API App: {match.Groups[2]} {match.Groups[3]}";
HttpUserAgentInformation client = HttpUserAgentParser.Parse(UserAgent);
StringBuilder sb = new($"{client.Name} {client.Version}");
if (client.Platform.HasValue)
sb.Append($" on {client.Platform.Value.PlatformType.ToString()}");
return sb.ToString();
}
public string GetDescription()
{
StringBuilder sb = new();
sb.AppendLine($"Created: {Created.Humanize(DateTimeOffset.Now)}");
sb.AppendLine($"Last seen: {LastSeen.Humanize(DateTimeOffset.Now)}");
Match match = Regex.Match(UserAgent, DatabaseManager.ApiUaRegex);
if (match.Success)
{
sb.AppendLine($"API version: {HttpUtility.HtmlEncode(match.Groups[1])}");
sb.AppendLine($"App info: {HttpUtility.HtmlEncode(match.Groups[4])}");
sb.AppendLine("Allowed scopes:");
foreach (string scope in Scopes) sb.AppendLine($"- {scope}");
}
return sb.ToString();
}
public async Task UpdateLastAccess(DateTimeOffset newTime)
{
await DatabaseManager.Logins.UpdateLastAccess(Identifier, newTime);
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using InnerTube.Models;
namespace LightTube.Database
{
public class LTPlaylist
{
public string Id;
public string Name;
public string Description;
public PlaylistVisibility Visibility;
public List<string> VideoIds;
public string Author;
public DateTimeOffset LastUpdated;
public async Task<YoutubePlaylist> ToYoutubePlaylist()
{
List<Thumbnail> t = new();
if (VideoIds.Count > 0)
t.Add(new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{VideoIds.First()}/maxresdefault.webp" });
YoutubePlaylist playlist = new()
{
Id = Id,
Title = Name,
Description = Description,
VideoCount = VideoIds.Count.ToString(),
ViewCount = "0",
LastUpdated = "Last updated " + LastUpdated.ToString("MMMM dd, yyyy"),
Thumbnail = t.ToArray(),
Channel = new Channel
{
Name = Author,
Id = GenerateChannelId(),
SubscriberCount = "0 subscribers",
Avatars = Array.Empty<Thumbnail>()
},
Videos = (await DatabaseManager.Playlists.GetPlaylistVideos(Id)).Select(x =>
{
x.Index = VideoIds.IndexOf(x.Id) + 1;
return x;
}).Cast<DynamicItem>().ToArray(),
ContinuationKey = null
};
return playlist;
}
private string GenerateChannelId()
{
StringBuilder sb = new("LTU-" + Author.Trim() + "_");
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Random rng = new(Author.GetHashCode());
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
return sb.ToString();
}
}
public enum PlaylistVisibility
{
PRIVATE,
UNLISTED,
VISIBLE
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml;
using MongoDB.Bson.Serialization.Attributes;
namespace LightTube.Database
{
[BsonIgnoreExtraElements]
public class LTUser
{
public string UserID;
public string PasswordHash;
public List<string> SubscribedChannels;
public bool ApiAccess;
public string RssToken;
public async Task<string> GenerateRssFeed(string hostUrl, int limit)
{
XmlDocument document = new();
XmlElement rss = document.CreateElement("rss");
rss.SetAttribute("version", "2.0");
XmlElement channel = document.CreateElement("channel");
XmlElement title = document.CreateElement("title");
title.InnerText = "LightTube subscriptions RSS feed for " + UserID;
channel.AppendChild(title);
XmlElement description = document.CreateElement("description");
description.InnerText = $"LightTube subscriptions RSS feed for {UserID} with {SubscribedChannels.Count} channels";
channel.AppendChild(description);
FeedVideo[] feeds = await YoutubeRSS.GetMultipleFeeds(SubscribedChannels);
IEnumerable<FeedVideo> feedVideos = feeds.Take(limit);
foreach (FeedVideo video in feedVideos)
{
XmlElement item = document.CreateElement("item");
XmlElement id = document.CreateElement("id");
id.InnerText = $"id:video:{video.Id}";
item.AppendChild(id);
XmlElement vtitle = document.CreateElement("title");
vtitle.InnerText = video.Title;
item.AppendChild(vtitle);
XmlElement vdescription = document.CreateElement("description");
vdescription.InnerText = video.Description;
item.AppendChild(vdescription);
XmlElement link = document.CreateElement("link");
link.InnerText = $"https://{hostUrl}/watch?v={video.Id}";
item.AppendChild(link);
XmlElement published = document.CreateElement("pubDate");
published.InnerText = video.PublishedDate.ToString("R");
item.AppendChild(published);
XmlElement author = document.CreateElement("author");
XmlElement name = document.CreateElement("name");
name.InnerText = video.ChannelName;
author.AppendChild(name);
XmlElement uri = document.CreateElement("uri");
uri.InnerText = $"https://{hostUrl}/channel/{video.ChannelId}";
author.AppendChild(uri);
item.AppendChild(author);
/*
XmlElement mediaGroup = document.CreateElement("media_group");
XmlElement mediaTitle = document.CreateElement("media_title");
mediaTitle.InnerText = video.Title;
mediaGroup.AppendChild(mediaTitle);
XmlElement mediaThumbnail = document.CreateElement("media_thumbnail");
mediaThumbnail.SetAttribute("url", video.Thumbnail);
mediaGroup.AppendChild(mediaThumbnail);
XmlElement mediaContent = document.CreateElement("media_content");
mediaContent.SetAttribute("url", $"https://{hostUrl}/embed/{video.Id}");
mediaContent.SetAttribute("type", "text/html");
mediaGroup.AppendChild(mediaContent);
item.AppendChild(mediaGroup);
*/
channel.AppendChild(item);
}
rss.AppendChild(channel);
document.AppendChild(rss);
return document.OuterXml;//.Replace("<media_", "<media:").Replace("</media_", "</media:");
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Xml;
using InnerTube.Models;
namespace LightTube.Database
{
public class LTVideo : PlaylistVideoItem
{
public string UploadedAt;
public long Views;
public override XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("duration", Duration);
item.SetAttribute("views", Views.ToString());
item.SetAttribute("uploadedAt", UploadedAt);
item.SetAttribute("index", Index.ToString());
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
if (Channel is not null)
item.AppendChild(Channel.GetXmlElement(doc));
foreach (Thumbnail t in Thumbnails ?? Array.Empty<Thumbnail>())
{
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.SetAttribute("width", t.Width.ToString());
thumbnail.SetAttribute("height", t.Height.ToString());
thumbnail.InnerText = t.Url;
item.AppendChild(thumbnail);
}
return item;
}
}
}

View File

@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using InnerTube.Models;
using MongoDB.Driver;
namespace LightTube.Database
{
public class LoginManager
{
private IMongoCollection<LTUser> _userCollection;
private IMongoCollection<LTLogin> _tokenCollection;
public LoginManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTLogin> tokenCollection)
{
_userCollection = userCollection;
_tokenCollection = tokenCollection;
}
public async Task<LTLogin> CreateToken(string email, string password, string userAgent, string[] scopes)
{
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
if (!await users.AnyAsync())
throw new UnauthorizedAccessException("Invalid credentials");
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
throw new UnauthorizedAccessException("Invalid credentials");
if (!scopes.Contains("web") && !user.ApiAccess)
throw new InvalidOperationException("This user has API access disabled");
LTLogin login = new()
{
Identifier = Guid.NewGuid().ToString(),
Email = email,
Token = GenerateToken(256),
UserAgent = userAgent,
Scopes = scopes.ToArray(),
Created = DateTimeOffset.Now,
LastSeen = DateTimeOffset.Now
};
await _tokenCollection.InsertOneAsync(login);
return login;
}
public async Task UpdateLastAccess(string id, DateTimeOffset offset)
{
LTLogin login = (await _tokenCollection.FindAsync(x => x.Identifier == id)).First();
login.LastSeen = offset;
await _tokenCollection.ReplaceOneAsync(x => x.Identifier == id, login);
}
public async Task RemoveToken(string token)
{
await _tokenCollection.FindOneAndDeleteAsync(t => t.Token == token);
}
public async Task RemoveToken(string email, string password, string identifier)
{
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
if (!await users.AnyAsync())
throw new KeyNotFoundException("Invalid credentials");
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
throw new UnauthorizedAccessException("Invalid credentials");
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier && t.Email == user.UserID);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public async Task RemoveTokenFromId(string sourceToken, string identifier)
{
LTLogin login = (await _tokenCollection.FindAsync(x => x.Token == sourceToken)).First();
LTLogin deletedLogin = (await _tokenCollection.FindAsync(x => x.Identifier == identifier)).First();
if (login.Email == deletedLogin.Email)
await _tokenCollection.FindOneAndDeleteAsync(t => t.Identifier == identifier);
else
throw new UnauthorizedAccessException(
"Logged in user does not match the token that is supposed to be deleted");
}
public async Task<LTUser> GetUserFromToken(string token)
{
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
return (await _userCollection.FindAsync(u => u.UserID == email)).First();
}
public async Task<LTUser> GetUserFromRssToken(string token) => (await _userCollection.FindAsync(u => u.RssToken == token)).First();
public async Task<LTLogin> GetLoginFromToken(string token)
{
var res = await _tokenCollection.FindAsync(x => x.Token == token);
return res.First();
}
public async Task<List<LTLogin>> GetAllUserTokens(string token)
{
string email = (await _tokenCollection.FindAsync(x => x.Token == token)).First().Email;
return await (await _tokenCollection.FindAsync(u => u.Email == email)).ToListAsync();
}
public async Task<string> GetCurrentLoginId(string token)
{
return (await _tokenCollection.FindAsync(t => t.Token == token)).First().Identifier;
}
public async Task<(LTChannel channel, bool subscribed)> SubscribeToChannel(LTUser user, YoutubeChannel channel)
{
LTChannel ltChannel = await DatabaseManager.Channels.UpdateChannel(channel.Id, channel.Name, channel.Subscribers,
channel.Avatars.FirstOrDefault()?.Url);
if (user.SubscribedChannels.Contains(ltChannel.ChannelId))
user.SubscribedChannels.Remove(ltChannel.ChannelId);
else
user.SubscribedChannels.Add(ltChannel.ChannelId);
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
return (ltChannel, user.SubscribedChannels.Contains(ltChannel.ChannelId));
}
public async Task SetApiAccess(LTUser user, bool access)
{
user.ApiAccess = access;
await _userCollection.ReplaceOneAsync(x => x.UserID == user.UserID, user);
}
public async Task DeleteUser(string email, string password)
{
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
if (!await users.AnyAsync())
throw new KeyNotFoundException("Invalid credentials");
LTUser user = (await _userCollection.FindAsync(x => x.UserID == email)).First();
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
throw new UnauthorizedAccessException("Invalid credentials");
await _userCollection.DeleteOneAsync(x => x.UserID == email);
await _tokenCollection.DeleteManyAsync(x => x.Email == email);
foreach (LTPlaylist pl in await DatabaseManager.Playlists.GetUserPlaylists(email))
await DatabaseManager.Playlists.DeletePlaylist(pl.Id);
}
public async Task CreateUser(string email, string password)
{
IAsyncCursor<LTUser> users = await _userCollection.FindAsync(x => x.UserID == email);
if (await users.AnyAsync())
throw new DuplicateNameException("A user with that email already exists");
LTUser user = new()
{
UserID = email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
SubscribedChannels = new List<string>(),
RssToken = GenerateToken(32)
};
await _userCollection.InsertOneAsync(user);
}
private string GenerateToken(int length)
{
string tokenAlphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-+*/()[]{}";
Random rng = new();
StringBuilder sb = new();
for (int i = 0; i < length; i++)
sb.Append(tokenAlphabet[rng.Next(0, tokenAlphabet.Length)]);
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using InnerTube;
using InnerTube.Models;
using MongoDB.Driver;
using Newtonsoft.Json.Linq;
namespace LightTube.Database
{
public class PlaylistManager
{
private IMongoCollection<LTUser> _userCollection;
private IMongoCollection<LTPlaylist> _playlistCollection;
private IMongoCollection<LTVideo> _videoCacheCollection;
private Youtube _youtube;
public PlaylistManager(IMongoCollection<LTUser> userCollection, IMongoCollection<LTPlaylist> playlistCollection,
IMongoCollection<LTVideo> videoCacheCollection, Youtube youtube)
{
_userCollection = userCollection;
_playlistCollection = playlistCollection;
_videoCacheCollection = videoCacheCollection;
_youtube = youtube;
}
public async Task<LTPlaylist> CreatePlaylist(LTUser user, string name, string description,
PlaylistVisibility visibility, string idPrefix = null)
{
if (await _userCollection.CountDocumentsAsync(x => x.UserID == user.UserID) == 0)
throw new UnauthorizedAccessException("Local accounts cannot create playlists");
LTPlaylist pl = new()
{
Id = GenerateAuthorId(idPrefix),
Name = name,
Description = description,
Visibility = visibility,
VideoIds = new List<string>(),
Author = user.UserID,
LastUpdated = DateTimeOffset.Now
};
await _playlistCollection.InsertOneAsync(pl).ConfigureAwait(false);
return pl;
}
public async Task<LTPlaylist> GetPlaylist(string id)
{
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Id == id);
return await cursor.FirstOrDefaultAsync() ?? new LTPlaylist
{
Id = null,
Name = "",
Description = "",
Visibility = PlaylistVisibility.VISIBLE,
VideoIds = new List<string>(),
Author = "",
LastUpdated = DateTimeOffset.MinValue
};
}
public async Task<List<LTVideo>> GetPlaylistVideos(string id)
{
LTPlaylist pl = await GetPlaylist(id);
List<LTVideo> videos = new();
foreach (string videoId in pl.VideoIds)
{
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == videoId);
videos.Add(await cursor.FirstAsync());
}
return videos;
}
public async Task<LTVideo> AddVideoToPlaylist(string playlistId, string videoId)
{
LTPlaylist pl = await GetPlaylist(playlistId);
YoutubeVideo vid = await _youtube.GetVideoAsync(videoId);
JObject ytPlayer = await InnerTube.Utils.GetAuthorizedPlayer(videoId, new HttpClient());
if (string.IsNullOrEmpty(vid.Id))
throw new KeyNotFoundException($"Couldn't find a video with ID '{videoId}'");
LTVideo v = new()
{
Id = vid.Id,
Title = vid.Title,
Thumbnails = ytPlayer?["videoDetails"]?["thumbnail"]?["thumbnails"]?.ToObject<Thumbnail[]>() ?? new []
{
new Thumbnail { Url = $"https://i.ytimg.com/vi_webp/{vid.Id}/maxresdefault.webp" }
},
UploadedAt = vid.UploadDate,
Views = long.Parse(vid.Views.Split(" ")[0].Replace(",", "").Replace(".", "")),
Channel = vid.Channel,
Duration = GetDurationString(ytPlayer?["videoDetails"]?["lengthSeconds"]?.ToObject<long>() ?? 0),
Index = pl.VideoIds.Count
};
pl.VideoIds.Add(vid.Id);
if (await _videoCacheCollection.CountDocumentsAsync(x => x.Id == vid.Id) == 0)
await _videoCacheCollection.InsertOneAsync(v);
else
await _videoCacheCollection.FindOneAndReplaceAsync(x => x.Id == vid.Id, v);
UpdateDefinition<LTPlaylist> update = Builders<LTPlaylist>.Update
.Push(x => x.VideoIds, vid.Id);
_playlistCollection.FindOneAndUpdate(x => x.Id == playlistId, update);
return v;
}
public async Task<LTVideo> RemoveVideoFromPlaylist(string playlistId, int videoIndex)
{
LTPlaylist pl = await GetPlaylist(playlistId);
IAsyncCursor<LTVideo> cursor = await _videoCacheCollection.FindAsync(x => x.Id == pl.VideoIds[videoIndex]);
LTVideo v = await cursor.FirstAsync();
pl.VideoIds.RemoveAt(videoIndex);
await _playlistCollection.FindOneAndReplaceAsync(x => x.Id == playlistId, pl);
return v;
}
public async Task<IEnumerable<LTPlaylist>> GetUserPlaylists(string userId)
{
IAsyncCursor<LTPlaylist> cursor = await _playlistCollection.FindAsync(x => x.Author == userId);
return cursor.ToEnumerable();
}
private string GetDurationString(long length)
{
string s = TimeSpan.FromSeconds(length).ToString();
while (s.StartsWith("00:") && s.Length > 5) s = s[3..];
return s;
}
public static string GenerateAuthorId(string prefix)
{
StringBuilder sb = new(string.IsNullOrWhiteSpace(prefix) || prefix.Trim().Length > 20
? "LT-PL"
: "LT-PL-" + prefix.Trim() + "_");
string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
Random rng = new();
while (sb.Length < 32) sb.Append(alphabet[rng.Next(0, alphabet.Length)]);
return sb.ToString();
}
public async Task DeletePlaylist(string playlistId)
{
await _playlistCollection.DeleteOneAsync(x => x.Id == playlistId);
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Xml;
namespace LightTube.Database
{
public class SubscriptionChannels
{
public LTChannel[] Channels { get; set; }
public XmlNode GetXmlDocument()
{
XmlDocument doc = new();
XmlElement feed = doc.CreateElement("Subscriptions");
foreach (LTChannel channel in Channels) feed.AppendChild(channel.GetXmlElement(doc));
doc.AppendChild(feed);
return doc;
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Xml;
namespace LightTube.Database
{
public class SubscriptionFeed
{
public FeedVideo[] videos;
public XmlDocument GetXmlDocument()
{
XmlDocument doc = new();
XmlElement feed = doc.CreateElement("Feed");
foreach (FeedVideo feedVideo in videos) feed.AppendChild(feedVideo.GetXmlElement(doc));
doc.AppendChild(feed);
return doc;
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\InnerTube\InnerTube.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MongoDB.Driver" Version="2.14.1" />
<PackageReference Include="MyCSharp.HttpUserAgentParser" Version="1.1.5" />
<PackageReference Include="YamlDotNet" Version="11.2.1" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
using System;
namespace LightTube.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

26
core/LightTube/Program.cs Normal file
View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace LightTube
{
public class Program
{
public static void Main(string[] args)
{
Configuration.LoadConfiguration();
InnerTube.Utils.SetAuthorization(Configuration.Instance.Credentials.CanUseAuthorizedEndpoints(),
Configuration.Instance.Credentials.Sapisid, Configuration.Instance.Credentials.Psid);
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View File

@@ -0,0 +1,60 @@
@using LightTube.Database
@using System.Web
@model LightTube.Contexts.BaseContext
@{
ViewBag.Title = "Account";
Layout = "_Layout";
Context.Request.Cookies.TryGetValue("theme", out string theme);
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
string newTheme = theme switch {
"light" => "dark",
"dark" => "light",
var _ => "dark"
};
bool compatibility = false;
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out compatibility);
}
<div class="login-container">
<div>
<div class="fullscreen-account-menu">
<h1>Settings</h1>
<br>
<div class="guide-item">
<a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Switch to @(newTheme) theme</a>
</div>
<br>
@if (Context.TryGetUser(out LTUser user, "web"))
{
<div class="guide-item">
<a href="/Account/Settings">Settings</a>
</div>
@if (user.PasswordHash != "local_account")
{
<div class="guide-item">
<a href="/Account/Logins">Active logins</a>
</div>
}
<div class="guide-item">
<a href="/Account/Logout">Log out</a>
</div>
}
else
{
<div class="guide-item">
<a href="/Account/Login">Log in</a>
</div>
<div class="guide-item">
<a href="/Account/Register">Register</a>
</div>
}
</div>
</div>
<div>
</div>
</div>

View File

@@ -0,0 +1,52 @@
@using System.Web
@using LightTube.Database
@model LightTube.Contexts.AddToPlaylistContext
@{
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
ViewBag.Metadata["og:title"] = Model.Video.Title;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Thumbnail)}";
ViewBag.Title = Model.Video.Title;
Layout = "_Layout";
}
<div class="playlist-page">
<div class="playlist-info">
<div class="thumbnail" style="background-image: url('@Model.Thumbnail')">
<a href="/watch?v=@Model.Video.Id">Watch</a>
</div>
<p class="title">@Model.Video.Title</p>
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
<div class="channel-info">
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
</a>
<div class="name">
<a class="name" href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
</div>
</div>
</div>
<div class="video-list playlist-list playlist-video-list">
<h3>Add to one of these playlists:</h3>
<a class="login-button" href="/Account/CreatePlaylist" style="margin:unset;">Create playlist</a>
@foreach (LTPlaylist playlist in Model.Playlists)
{
<div class="playlist-video">
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="thumbnail"
style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
</a>
<div class="info">
<a href="/playlist?list=@playlist.Id&add=@Model.Id" class="title max-lines-2">
@playlist.Name
</a>
<div>
<span>@playlist.VideoIds.Count videos</span>
</div>
</div>
</div>
}
</div>
</div>

View File

@@ -0,0 +1,25 @@
@model LightTube.Contexts.BaseContext
@{
ViewBag.Title = "Create Playlist";
Layout = "_Layout";
}
<div class="login-container">
<div>
<div>
<form asp-action="CreatePlaylist" method="POST" class="playlist-form">
<h1>Create Playlist</h1>
<input name="name" type="text" placeholder="Playlist Name">
<input name="description" type="text" placeholder="Description">
<select name="visibility">
<option value="UNLISTED">Anyone with the link can view</option>
<option value="PRIVATE">Only you can view</option>
</select>
<input type="submit" value="Create">
</form>
</div>
</div>
<div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
@using LightTube.Database
@model LightTube.Contexts.MessageContext
@{
ViewData["Title"] = "Delete Account";
Layout = "_Layout";
}
@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="login-message">
@Model.Message
</div>
}
<div class="login-container">
<div>
<div>
@if (Context.Request.Cookies.TryGetValue("account_data", out string _))
{
Context.TryGetUser(out LTUser user, "web");
<form asp-action="Delete" method="POST" class="login-form">
<h1>Delete Account</h1>
<p>Deleting a local account</p>
<input name="email" type="hidden" value="@user.UserID">
<input name="password" type="hidden" value="@user.PasswordHash">
<input type="submit" value="Delete Account" class="login-button danger">
</form>
}
else
{
<form asp-action="Delete" method="POST" class="login-form">
<h1>Delete Account</h1>
<input name="userid" type="text" placeholder="UserID">
<input name="password" type="password" placeholder="Password">
<input type="submit" value="Delete Account" class="login-button danger">
</form>
}
</div>
</div>
<div>
<div>
<h1>Warning!</h1>
<p>You cannot undo this operation! After you enter your username and password, your account will get deleted forever.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
@model LightTube.Contexts.MessageContext
@{
ViewData["Title"] = "Login";
Layout = "_Layout";
}
@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="login-message">
@Model.Message
</div>
}
<div class="login-container">
<div>
<div>
<form asp-action="Login" method="POST" class="login-form">
<h1>Log in</h1>
<input name="userid" type="text" placeholder="UserID">
<input name="password" type="password" placeholder="Password">
<input type="submit" value="Login">
</form>
</div>
</div>
<div>
<h2>Don't have an account?</h2>
<a href="/Account/Register" class="login-button">Create an account</a>
</div>
</div>

View File

@@ -0,0 +1,18 @@
@using LightTube.Database
@model LightTube.Contexts.LoginsContext
@{
ViewData["Title"] = "Active Logins";
Layout = "_Layout";
}
<h1 style="text-align:center;">Active Logins</h1>
<div class="logins-container">
@foreach (LTLogin login in Model.Logins)
{
<div class="login">
<h2 class="max-lines-1">@(login.Identifier == Model.CurrentLogin ? "(This window) " : "")@login.GetTitle()</h2>
<p>@Html.Raw(login.GetDescription().Replace("\n", "<br>"))</p>
<a href="/Account/DisableLogin?id=@login.Identifier" class="login-button" style="color:red;">Disable</a>
</div>
}
</div>

View File

@@ -0,0 +1,42 @@
@model LightTube.Contexts.MessageContext
@{
ViewData["Title"] = "Register";
Layout = "_Layout";
}
@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="login-message">
@Model.Message
</div>
}
<div class="login-container">
<div>
<div>
<form asp-action="Register" method="POST" class="login-form">
<h1>Register</h1>
<input name="userid" type="text" placeholder="UserID">
<input name="password" type="password" placeholder="Password">
<input type="submit" value="Register">
</form>
</div>
</div>
<div>
<div>
<h1>...or register with a local account</h1>
<h2>What is the difference?</h2>
<ul>
<li>Remote account data is saved in this lighttube instance, while local account data is stored in
your browser's cookies
<ul>
<li>This means that the author of this lighttube instance cannot see your account data</li>
<li>It also means that, if you clear your cookies a lot, your account data will also get
lost with the cookies</li>
</ul>
</li>
</ul>
<a href="/Account/RegisterLocal" class="login-button">Create local account</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,63 @@
@model LightTube.Contexts.SettingsContext
@{
ViewBag.Title = "Settings";
Layout = "_Layout";
}
<form method="post">
<div class="settings-content">
<h1 style="text-align:center">Settings</h1>
<div>
<label for="settings-theme">Theme</label>
<select id="settings-theme" name="theme">
@Html.Raw($"<option value='light' {(Model.Theme == "light" ? "selected" : "")}>Light</option>")
@Html.Raw($"<option value='dark' {(Model.Theme == "dark" ? "selected" : "")}>Dark</option>")
</select>
<p>This is the visual theme the website will use.</p>
</div>
<div>
<label for="settings-yhl">Content Language</label>
<select id="settings-yhl" name="hl">
@foreach (KeyValuePair<string, string> o in Model.Languages)
{
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentLanguage ? "selected" : "")}>{o.Value}</option>")
}
</select>
<p>The language YouTube will deliver the content in. This will not affect LightTube's UI language.</p>
</div>
<div>
<label for="settings-ygl">Content Region</label>
<select id="settings-ygl" name="gl">
@foreach (KeyValuePair<string, string> o in Model.Regions)
{
@Html.Raw($"<option value='{o.Key}' {(o.Key == Model.CurrentRegion ? "selected" : "")}>{o.Value}</option>")
}
</select>
<p>The language YouTube will deliver the content for. It is used for the explore page and the recommendations.</p>
</div>
<div>
<label for="settings-player">Player</label>
<select id="settings-player" name="compatibility">
@Html.Raw($"<option value=\"false\" {(Model.CompatibilityMode ? "" : "selected")}>DASH playback with muxed fallback (recommended)</option>")
@Html.Raw($"<option value=\"true\" {(Model.CompatibilityMode ? "selected" : "")}>Muxed formats only (only supports 360p & 720p)</option>")
</select>
<p>Player behaviour. DASH playback allows for resolutions over 720p, but it is not compatible in all browsers. (e.g: Firefox Mobile)</p>
</div>
<div>
<label for="settings-api">API Access</label>
<select id="settings-api" name="api-access">
@Html.Raw($"<option value=\"true\" {(Model.ApiAccess ? "selected" : "")}>Enabled</option>")
@Html.Raw($"<option value=\"false\" {(Model.ApiAccess ? "" : "selected")}>Disabled</option>")
</select>
<p>This will allow apps to log in using your username and password</p>
</div>
<div style="display:flex;flex-direction:row">
<a href="/Account/Logins" class="login-button">Active Logins</a>
<a href="/Account/Delete" class="login-button" style="color:red">Delete Account</a>
</div>
</div>
<br>
<input type="submit" class="login-button" value="Save"/>
</form>

View File

@@ -0,0 +1,26 @@
@using LightTube.Database
@model LightTube.Contexts.FeedContext
@{
ViewBag.Title = "Channel list";
}
<div class="video-list">
@foreach (LTChannel channel in Model.Channels)
{
<div class="channel">
<a href="/channel/@channel.ChannelId" class="avatar">
<img src="@channel.IconUrl" alt="Channel Avatar">
</a>
<a href="/channel/@channel.ChannelId" class="info">
<span class="name max-lines-2">@channel.Name</span>
<div>
<div>
<span>@channel.Subscribers</span>
</div>
</div>
</a>
<button class="subscribe-button" data-cid="@channel.ChannelId">Subscribe</button>
</div>
}
</div>

View File

@@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Explore";
ViewData["SelectedGuideItem"] = "explore";
}
<div style="text-align: center">
<h1>Coming soon!</h1>
</div>

View File

@@ -0,0 +1,35 @@
@using LightTube.Database
@model LightTube.Contexts.PlaylistsContext
@{
ViewData["Title"] = "Playlists";
ViewData["SelectedGuideItem"] = "library";
Layout = "_Layout";
}
<div class="video-list">
<h2>Playlists</h2>
@foreach (LTPlaylist playlist in Model.Playlists)
{
<div class="playlist">
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="thumbnail" style="background-image: url('https://i.ytimg.com/vi_webp/@playlist.VideoIds.FirstOrDefault()/maxresdefault.webp')">
<div>
<span>@playlist.VideoIds.Count</span><span>VIDEOS</span>
</div>
</a>
<div class="info">
<a href="/watch?v=@playlist.VideoIds.FirstOrDefault()&list=@playlist.Id" class="title max-lines-2">@playlist.Name</a>
<div>
<a href="/channel/@PlaylistManager.GenerateAuthorId(playlist.Author)">@playlist.Author</a>
<ul>
<li>
<a href="/playlist?list=@playlist.Id">
<b>View Full Playlist</b>
</a>
</li>
</ul>
</div>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,55 @@
@using Humanizer
@using LightTube.Database
@using System.Web
@model LightTube.Contexts.FeedContext
@{
ViewData["Title"] = "Subscriptions";
ViewData["SelectedGuideItem"] = "subs";
bool minMode = false;
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
bool.TryParse(minModeString, out minMode);
}
<div class="horizontal-channel-list" style="max-width: @(!Model.MobileLayout ? $"calc(100vw - {(minMode ? 80 : 312)}px);" : "")">
<a href="/feed/channels" class="channel">
<i class="bi bi-gear"></i>
<div class="name max-lines-2">Manage Channels</div>
</a>
<a href="/rss?token=@HttpUtility.UrlEncode(Model.RssToken)" class="channel">
<i class="bi bi-rss"></i>
<div class="name max-lines-2">RSS Feed</div>
</a>
@foreach (LTChannel channel in Model.Channels)
{
<a href="/channel/@channel.ChannelId" class="channel">
<img src="@channel.IconUrl" loading="lazy">
<div class="name max-lines-2">@channel.Name</div>
</a>
}
</div>
<div class="rich-video-grid">
@foreach (FeedVideo video in Model.Videos)
{
<div class="video">
<a href="/watch?v=@video.Id" class="thumbnail img-thumbnail">
<img src="@video.Thumbnail" loading="lazy">
</a>
<a href="/channel/@video.ChannelId" class="avatar">
<img src="@Model.Channels.First(x => x.ChannelId == video.ChannelId).IconUrl">
</a>
<div class="info">
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
<div>
<a href="/channel/@video.ChannelId">@video.ChannelName</a>
<div>
<span>@video.ViewCount views</span>
<span>•</span>
<span>@video.PublishedDate.Humanize(DateTimeOffset.Now)</span>
</div>
</div>
</div>
</div>
}
</div>

View File

@@ -0,0 +1,15 @@
@model LightTube.Contexts.BaseContext
@{
ViewBag.Metadata = new Dictionary<string, string>
{
["og:title"] = "LightTube",
["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}",
["og:description"] = "An alternative, privacy respecting front end for YouTube",
};
ViewData["Title"] = "Home Page";
ViewData["SelectedGuideItem"] = "home";
}
<div style="text-align: center">
<h1>@Configuration.Instance.Interface.MessageOfTheDay</h1>
</div>

View File

@@ -0,0 +1,17 @@
@model LightTube.Contexts.ErrorContext
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<p>
You can try other alternatives to access this resource such as:
<b>
<a href="https://invidio.us@($"{Model.Path}{Context.Request.QueryString}")">Invidious</a>
</b>
or
<b>
<a href="https://youtube.com@($"{Model.Path}{Context.Request.QueryString}")">YouTube</a>
</b>
</p>

View File

@@ -0,0 +1,139 @@
@using System.Web
@using LightTube.Contexts
@model LightTube.Contexts.BaseContext
@{
bool compatibility = false;
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out compatibility);
bool minMode = false;
if (Context.Request.Cookies.TryGetValue("minMode", out string minModeString))
bool.TryParse(minModeString, out minMode);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta property="og:site_name" content="lighttube" />
<meta property="og:type" content="website" />
@if (ViewBag.Metadata is not null)
{
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
{
if (metaTag.Key.StartsWith("og:"))
{
<meta property="@metaTag.Key" content="@metaTag.Value"/>
}
else
{
<meta name="@metaTag.Key" content="@metaTag.Value"/>
}
}
}
<meta property="theme-color" content="#AA0000" />
<title>@ViewData["Title"] - lighttube</title>
@if ((ViewData["HideGuide"] ?? false).Equals(true))
{
<style> .guide { display: none !important; } </style>
}
@{
Context.Request.Cookies.TryGetValue("theme", out string theme);
if (!new[] { "light", "dark" }.Contains(theme)) theme = "light";
}
<link rel="stylesheet" href="@($"~/css/colors-{theme}.css")" asp-append-version="true"/>
@if (Model.MobileLayout)
{
<link rel="stylesheet" href="~/css/mobile.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/css/lt-video/player-mobile.css" asp-append-version="true"/>
}
else
{
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
}
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css" asp-append-version="true"/>
<link rel="icon" href="~/favicon.ico">
</head>
<body>
<div class="top-bar @(ViewData["UseFullSizeSearchBar"]?.Equals(true) ?? false ? "full-size-search" : "")">
<a class="logo" href="/">light<b>tube</b></a>
<div class="divider"></div>
<form action="/results">
<input type="text" placeholder="Search" name="search_query" value="@(Model is SearchContext ctx ? ctx.Query : Context.Request.Cookies.TryGetValue("search_query", out string s) ? s : "")">
<input type="submit" value="Search">
</form>
<div class="divider"></div>
<div class="search-button">
<a class="icon-link" href="/results">
<i class="bi bi-search"></i>
</a>
</div>
<div class="account" tabindex="-1">
<a class="icon-link" href="/Account">
<i class="bi bi-person-circle"></i>
</a>
<div class="account-menu">
@Html.Partial("_LoginLogoutPartial")
<div class="guide-item"><a href="/toggles/theme?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">Toggle Theme</a></div>
</div>
</div>
</div>
<div class="guide @(minMode ? "minmode" : "")">
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "home" ? "active" : "")">
<a href="/">
<i class="icon bi bi-house-door"></i>
Home
</a>
</div>
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "explore" ? "active" : "")">
<a href="/feed/explore">
<i class="icon bi bi-compass"></i>
Explore
</a>
</div>
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "subs" ? "active" : "")">
<a href="/feed/subscriptions">
<i class="icon bi bi-inboxes"></i>
Subscriptions
</a>
</div>
<div class="guide-item @(ViewData["SelectedGuideItem"] as string == "library" ? "active" : "")">
<a href="/feed/library">
<i class="icon bi bi-list-ul"></i>
Library
</a>
</div>
<div class="hide-on-minmode guide-item">
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
<i class="icon"><i class="bi bi-arrow-left-square"></i></i>
Collapse Guide
</a>
</div>
<div class="show-on-minmode guide-item">
<a href="/toggles/collapse_guide?redirectUrl=@(HttpUtility.UrlEncode($"{Context.Request.Path}{Context.Request.QueryString}"))">
<i class="icon"><i class="bi bi-arrow-right-square"></i></i>
Expand
</a>
</div>
<hr class="hide-on-minmode">
<p class="hide-on-minmode">
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/README.md">About</a><br>
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/OTHERLIBS.md">How LightTube works</a><br>
<a href="https://gitlab.com/kuylar/lighttube">Source code</a>
<a href="https://gitlab.com/kuylar/lighttube/-/wikis/XML-API">API</a>
<a href="https://gitlab.com/kuylar/lighttube/-/blob/master/LICENSE">License</a><br>
<span style="font-weight: normal">Running on LightTube v@(Utils.GetVersion())</span>
</p>
</div>
<div class="app">
@RenderBody()
</div>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

View File

@@ -0,0 +1,16 @@
@using LightTube.Database
@if (Context.TryGetUser(out LTUser user, "web"))
{
<div class="guide-item"><a>@user.UserID.Split("@")[0]</a></div>
@if (user.PasswordHash != "local_account")
{
<div class="guide-item"><a href="/Account/Logins">Active logins</a></div>
}
<div class="guide-item"><a href="/Account/Logout">Log out</a></div>
}
else
{
<div class="guide-item"><a href="/Account/Login">Log in</a></div>
<div class="guide-item"><a href="/Account/Register">Register</a></div>
}
<div class="guide-item"><a href="/Account/Settings">Settings</a></div>

View File

@@ -0,0 +1,80 @@
@using InnerTube.Models
@using System.Web
@model LightTube.Contexts.ChannelContext
@{
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["og:title"] = Model.Channel.Name;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.FirstOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Channel.Avatars.LastOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["og:description"] = Model.Channel.Description;
ViewBag.Title = Model.Channel.Name;
Layout = "_Layout";
DynamicItem[] contents;
try
{
contents = ((ItemSectionItem)((ItemSectionItem)Model.Channel.Videos[0]).Contents[0]).Contents;
}
catch
{
contents = Model.Channel.Videos;
}
}
<div class="channel-page">
@if (Model.Channel.Banners.Length > 0)
{
<img class="channel-banner" alt="Channel Banner" src="@Model.Channel.Banners.Last().Url">
}
<div class="channel-info-container">
<div class="channel-info">
<a href="/channel/@Model.Channel.Id" class="avatar">
<img src="@Model.Channel.Avatars.LastOrDefault()?.Url" alt="Channel Avatar">
</a>
<div class="name">
<a>@Model.Channel.Name</a>
<span>@Model.Channel.Subscribers</span>
</div>
<button class="subscribe-button" data-cid="@Model.Channel.Id">Subscribe</button>
</div>
</div>
<h3>About</h3>
<p>@Html.Raw(Model.Channel.GetHtmlDescription())</p>
<br><br>
<h3>Uploads</h3>
<div class="video-grid">
@foreach (VideoItem video in contents.Where(x => x is VideoItem).Cast<VideoItem>())
{
<a href="/watch?v=@video.Id" class="video">
<div class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')"><span class="video-length">@video.Duration</span></div>
<div class="info">
<span class="title max-lines-2">@video.Title</span>
<div>
<div>
<span>@video.Views views</span>
<span>@video.UploadedAt</span>
</div>
</div>
</div>
</a>
}
</div>
<div class="pagination-buttons">
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
{
<a href="/channel?id=@Model.Id">First Page</a>
}
<div class="divider"></div>
<span>•</span>
<div class="divider"></div>
@if (!string.IsNullOrWhiteSpace(contents.FirstOrDefault(x => x is ContinuationItem)?.Id))
{
<a href="/channel/@Model.Id?continuation=@(contents.FirstOrDefault(x => x is ContinuationItem)?.Id)">Next Page</a>
}
</div>
</div>

View File

@@ -0,0 +1,95 @@
@using System.Web
@using InnerTube
@using InnerTube.Models
@model LightTube.Contexts.PlayerContext
@{
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
ViewBag.Metadata["og:title"] = Model.Player.Title;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
ViewBag.Title = Model.Player.Title;
Layout = "_Layout";
}
<div class="playlist-page">
<div class="playlist-info">
<div class="thumbnail" style="background-image: url('@Model.Player.Thumbnails.Last().Url')">
<a href="/watch?v=@Model.Player.Id">Watch</a>
</div>
<p class="title">@Model.Player.Title</p>
<span class="info">@Model.Video.Views • @Model.Video.UploadDate</span>
<div class="channel-info">
<a href="/channel/@Model.Player.Channel.Id" class="avatar">
<img src="@Model.Player.Channel.Avatars.LastOrDefault()?.Url">
</a>
<div class="name">
<a class="name" href="/channel/@Model.Player.Channel.Id">@Model.Player.Channel.Name</a>
</div>
</div>
</div>
<div class="video-list download-list playlist-video-list">
<div class="format-list">
<h2>Muxed formats</h2>
<p>These downloads have both video and audio in them</p>
@foreach (Format format in Model.Player.Formats)
{
<div class="download-format">
<div>
@format.FormatNote
</div>
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
<i class="bi bi-download"></i>
Download through LightTube
</a>
<a href="@format.Url">
<i class="bi bi-cloud-download"></i>
Download through YouTube
</a>
</div>
}
</div>
<div class="format-list">
<h2>Audio only formats</h2>
<p>These downloads have only have audio in them</p>
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.VideoCodec == "none"))
{
<div class="download-format">
<div>
@format.FormatNote (Codec: @format.AudioCodec, Sample Rate: @format.AudioSampleRate)
</div>
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
<i class="bi bi-download"></i>
Download through LightTube
</a>
<a href="@format.Url">
<i class="bi bi-cloud-download"></i>
Download through YouTube
</a>
</div>
}
</div>
<div class="format-list">
<h2>Video only formats</h2>
<p>These downloads have only have video in them</p>
@foreach (Format format in Model.Player.AdaptiveFormats.Where(x => x.AudioCodec == "none"))
{
<div class="download-format">
<div>
@format.FormatNote (Codec: @format.VideoCodec)
</div>
<a href="/proxy/download/@Model.Video.Id/@format.FormatId/@(HttpUtility.UrlEncode(Model.Video.Title)).@format.GetExtension()">
<i class="bi bi-download"></i>
Download through LightTube
</a>
<a href="@format.Url">
<i class="bi bi-cloud-download"></i>
Download through YouTube
</a>
</div>
}
</div>
</div>
</div>

View File

@@ -0,0 +1,146 @@
@using System.Collections.Specialized
@using System.Web
@using InnerTube.Models
@model LightTube.Contexts.PlayerContext
@{
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
ViewBag.Metadata["og:title"] = Model.Player.Title;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["og:description"] = Model.Player.Description;
ViewBag.Title = Model.Player.Title;
Layout = null;
try
{
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
}
catch
{
}
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
bool canPlay = true;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta property="og:site_name" content="lighttube"/>
<meta property="og:type" content="website"/>
@if (ViewBag.Metadata is not null)
{
@foreach (KeyValuePair<string, string> metaTag in ViewBag.Metadata)
{
if (metaTag.Key.StartsWith("og:"))
{
<meta property="@metaTag.Key" content="@metaTag.Value"/>
}
else
{
<meta name="@metaTag.Key" content="@metaTag.Value"/>
}
}
}
<meta property="theme-color" content="#AA0000"/>
<title>@ViewData["Title"] - lighttube</title>
<link rel="stylesheet" href="~/css/bootstrap-icons/bootstrap-icons.css"/>
<link rel="stylesheet" href="~/css/desktop.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/css/lt-video/player-desktop.css" asp-append-version="true"/>
<link rel="icon" href="~/favicon.ico">
</head>
<body>
@if (live)
{
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
</video>
}
else if (Model.Player.Formats.Length > 0)
{
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
{
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
}
</video>
}
else
{
canPlay = false;
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
{
<span>
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
</span>
}
else
{
<span>
@Model.Player.ErrorMessage
</span>
}
</div>
}
@if (canPlay)
{
<script src="/js/lt-video/player-desktop.js"></script>
@if (!Model.CompatibilityMode && !live)
{
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
<script>
let player = undefined;
loadPlayerWithShaka("video", {
"id": "@Model.Video.Id",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": true,
"live": false,
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
}, [
@foreach(Format f in Model.Player.Formats.Reverse())
{
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
}
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);;
</script>
}
else if (live)
{
<script src="/js/hls.js/hls.min.js"></script>
<script>
let player = undefined;
loadPlayerWithHls("video", {
"id": "@(Model.Video.Id)",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": true,
"live": true
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
</script>
}
else
{
<script>
const player = new Player("video", {
"id": "@Model.Video.Id",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": true,
"live": false,
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
}, [
@foreach(Format f in Model.Player.Formats.Reverse())
{
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
}
]);
</script>
}
}
</body>
</html>

View File

@@ -0,0 +1,85 @@
@using InnerTube.Models
@using System.Web
@model LightTube.Contexts.PlaylistContext
@{
ViewBag.Title = Model.Playlist.Title;
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["og:title"] = Model.Playlist.Title;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.FirstOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Playlist.Thumbnail.LastOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["og:description"] = Model.Playlist.Description;
Layout = "_Layout";
}
@if (!string.IsNullOrWhiteSpace(Model.Message))
{
<div class="playlist-message" style="padding: 16px;background-color: var(--border-color); color: var(--text-primary);">
@Model.Message
</div>
}
<div class="playlist-page">
<div class="playlist-info">
<div class="thumbnail" style="background-image: url('@Model.Playlist.Thumbnail.LastOrDefault()?.Url')">
<a href="/watch?v=@Model.Playlist.Videos.FirstOrDefault()?.Id&list=@Model.Id">Play all</a>
</div>
<p class="title">@Model.Playlist.Title</p>
<span class="info">@Model.Playlist.VideoCount videos • @Model.Playlist.ViewCount views • @Model.Playlist.LastUpdated</span>
<span class="description">@Html.Raw(Model.Playlist.GetHtmlDescription())</span>
<a href="/playlist?list=@Model.Id&remove=true" class="login-button" style="margin:unset;">
<i class="bi bi-trash"></i>
Delete playlist
</a>
<div class="channel-info">
<a href="/channel/@Model.Playlist.Channel.Id" class="avatar">
<img src="@Model.Playlist.Channel.Avatars.LastOrDefault()?.Url">
</a>
<div class="name">
<a class="name" href="/channel/@Model.Playlist.Channel.Id">@Model.Playlist.Channel.Name</a>
</div>
<button class="subscribe-button" data-cid="@Model.Playlist.Channel.Id">Subscribe</button>
</div>
</div>
<div class="video-list playlist-video-list">
@foreach (PlaylistVideoItem video in Model.Playlist.Videos.Cast<PlaylistVideoItem>())
{
<div class="playlist-video">
<a href="/watch?v=@video.Id&list=@Model.Id" class="index">
@video.Index
</a>
<a href="/watch?v=@video.Id&list=@Model.Id" class="thumbnail"
style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
<span class="video-length">@video.Duration</span>
</a>
<div class="info">
<a href="/watch?v=@video.Id&list=@Model.Id" class="title max-lines-2">
@video.Title
</a>
<div>
<a href="/channel/@video.Channel.Name">@video.Channel.Name</a>
</div>
</div>
@if (Model.Editable)
{
<a href="/playlist?list=@Model.Id&delete=@(video.Index - 1)" class="edit">
<i class="bi bi-trash"></i>
</a>
}
</div>
}
</div>
</div>
<div class="pagination-buttons">
@if (!string.IsNullOrWhiteSpace(Model.ContinuationToken))
{
<a href="/playlist?list=@Model.Id">First Page</a>
}
<div class="divider"></div>
<span>•</span>
<div class="divider"></div>
@if (!string.IsNullOrWhiteSpace(Model.Playlist.ContinuationKey))
{
<a href="/playlist?list=@Model.Id&continuation=@Model.Playlist.ContinuationKey">Next Page</a>
}
</div>

View File

@@ -0,0 +1,28 @@
@using InnerTube.Models
@model LightTube.Contexts.SearchContext
@{
ViewBag.Title = Model.Query;
Layout = "_Layout";
ViewData["UseFullSizeSearchBar"] = Model.MobileLayout;
}
<div class="video-list">
@foreach (DynamicItem preview in Model.Results.Results)
{
@preview.GetHtml()
}
</div>
<div class="pagination-buttons">
@if (!string.IsNullOrWhiteSpace(Model.ContinuationKey))
{
<a href="/results?search_query=@Model.Query">First Page</a>
}
<div class="divider"></div>
<span>•</span>
<div class="divider"></div>
@if (!string.IsNullOrWhiteSpace(Model.Results.ContinuationKey))
{
<a href="/results?search_query=@Model.Query&continuation=@Model.Results.ContinuationKey">Next Page</a>
}
</div>

View File

@@ -0,0 +1,325 @@
@using System.Text.RegularExpressions
@using System.Web
@using InnerTube.Models
@model LightTube.Contexts.PlayerContext
@{
bool compatibility = false;
if (Context.Request.Cookies.TryGetValue("compatibility", out string compatibilityString))
bool.TryParse(compatibilityString, out compatibility);
ViewBag.Metadata = new Dictionary<string, string>();
ViewBag.Metadata["author"] = Model.Video.Channel.Name;
ViewBag.Metadata["og:title"] = Model.Player.Title;
ViewBag.Metadata["og:url"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}{Url.ActionContext.HttpContext.Request.Path}{Url.ActionContext.HttpContext.Request.QueryString}";
ViewBag.Metadata["og:image"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.FirstOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["twitter:card"] = $"{Url.ActionContext.HttpContext.Request.Scheme}://{Url.ActionContext.HttpContext.Request.Host}/proxy/image?url={HttpUtility.UrlEncode(Model.Player.Thumbnails.LastOrDefault()?.Url?.ToString())}";
ViewBag.Metadata["og:description"] = Model.Player.Description;
ViewBag.Title = Model.Player.Title;
Layout = "_Layout";
try
{
ViewBag.Metadata["og:video"] = $"/proxy/video?url={HttpUtility.UrlEncode(Model.Player.Formats.First().Url.ToString())}";
Model.Resolution ??= Model.Player.Formats.First().FormatNote;
}
catch
{
}
ViewData["HideGuide"] = true;
bool live = Model.Player.Formats.Length == 0 && Model.Player.AdaptiveFormats.Length > 0;
string description = Model.Video.GetHtmlDescription();
const string youtubePattern = @"[w.]*youtube[-nockie]*\.com";
// turn URLs into hyperlinks
Regex urlRegex = new(youtubePattern, RegexOptions.IgnoreCase);
Match m;
for (m = urlRegex.Match(description); m.Success; m = m.NextMatch())
description = description.Replace(m.Groups[0].ToString(),
$"{Url.ActionContext.HttpContext.Request.Host}");
bool canPlay = true;
}
<!-- TODO: chapters -->
<div class="watch-page">
<div class="primary">
<div class="video-player-container">
@if (live)
{
<video class="player" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
</video>
}
else if (Model.Player.Formats.Length > 0)
{
<video class="player" controls src="/proxy/media/@Model.Player.Id/@HttpUtility.UrlEncode(Model.Player.Formats.First(x => x.FormatNote == Model.Resolution && x.FormatId != "17").FormatId)" poster="@Model.Player.Thumbnails.LastOrDefault()?.Url">
@foreach (Subtitle subtitle in Model.Player.Subtitles ?? Array.Empty<Subtitle>())
{
@:<track src="/proxy/caption/@Model.Player.Id/@HttpUtility.UrlEncode(subtitle.Language).Replace("+", "%20")" label="@subtitle.Language" kind="subtitles">
}
</video>
}
else
{
canPlay = false;
<div id="player" class="player error" style="background-image: url('@Model.Player.Thumbnails.LastOrDefault()?.Url')">
@if (string.IsNullOrWhiteSpace(Model.Player.ErrorMessage))
{
<span>
No playable streams returned from the API (@Model.Player.Formats.Length/@Model.Player.AdaptiveFormats.Length)
</span>
}
else
{
<span>
@Model.Player.ErrorMessage
</span>
}
</div>
}
</div>
@if (Model.MobileLayout)
{
<div class="video-info">
<div class="video-title">@Model.Video.Title</div>
<div class="video-info-bar">
<span>@Model.Video.Views</span>
<span>Published @Model.Video.UploadDate</span>
<div class="divider"></div>
<div class="video-info-buttons">
<div>
<i class="bi bi-hand-thumbs-up"></i><span>@Model.Engagement.Likes</span>
</div>
<div>
<i class="bi bi-hand-thumbs-down"></i><span>@Model.Engagement.Dislikes</span>
</div>
<a href="/download?v=@Model.Video.Id">
<i class="bi bi-download"></i>
Download
</a>
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
<i class="bi bi-folder-plus"></i>
Save
</a>
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
<i class="bi bi-share"></i>
YouTube link
</a>
</div>
</div>
<div class="channel-info">
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
<img src="@Model.Video.Channel.Avatars.LastOrDefault()?.Url">
</a>
<div class="name">
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
</div>
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
</div>
<p class="description">@Html.Raw(description)</p>
</div>
<hr>
}
else
{
<div class="video-info">
<div class="video-title">@Model.Video.Title</div>
<p class="video-sub-info description">
<span>@Model.Video.Views&nbsp; @Model.Video.UploadDate</span>&nbsp; @Html.Raw(description)
</p>
<div class="video-info-buttons">
<div>
<i class="bi bi-hand-thumbs-up"></i>
@Model.Engagement.Likes
</div>
<div>
<i class="bi bi-hand-thumbs-down"></i>
@Model.Engagement.Dislikes
</div>
<a href="/download?v=@Model.Player.Id">
<i class="bi bi-download"></i>
Download
</a>
<a href="/Account/AddVideoToPlaylist?v=@Model.Video.Id">
<i class="bi bi-folder-plus"></i>
Save
</a>
<a href="https://www.youtube.com/watch?v=@Model.Video.Id">
<i class="bi bi-share"></i>
YouTube link
</a>
</div>
</div>
<div class="channel-info__bordered">
<a href="/channel/@Model.Video.Channel.Id" class="avatar">
<img src="@Model.Video.Channel.Avatars.FirstOrDefault()?.Url">
</a>
<div class="name">
<a href="/channel/@Model.Video.Channel.Id">@Model.Video.Channel.Name</a>
</div>
<div class="subscriber-count">
@Model.Video.Channel.SubscriberCount
</div>
<button class="subscribe-button" data-cid="@Model.Video.Channel.Id">Subscribe</button>
</div>
}
</div>
<div class="secondary">
<noscript>
<div class="resolutions-list">
<h3>Change Resolution</h3>
<div>
@foreach (Format format in Model.Player.Formats.Where(x => x.FormatId != "17"))
{
@if (format.FormatNote == Model.Resolution)
{
<b>@format.FormatNote (current)</b>
}
else
{
<a href="/watch?v=@Model.Player.Id&quality=@format.FormatNote">@format.FormatNote</a>
}
}
</div>
</div>
</noscript>
<div class="recommended-list">
@if (Model.Video.Recommended.Length == 0)
{
<p style="text-align: center">None :(<br>This is most likely an age-restricted video</p>
}
@foreach (DynamicItem recommendation in Model.Video.Recommended)
{
switch (recommendation)
{
case VideoItem video:
<div class="video">
<a href="/watch?v=@video.Id" class="thumbnail" style="background-image: url('@video.Thumbnails.LastOrDefault()?.Url')">
<span class="video-length">@video.Duration</span>
</a>
<div class="info">
<a href="/watch?v=@video.Id" class="title max-lines-2">@video.Title</a>
<div>
<a href="/channel/@video.Channel.Id" class="max-lines-1">@video.Channel.Name</a>
<div>
<span>@video.Views views</span>
<span>•</span>
<span>@video.UploadedAt</span>
</div>
</div>
</div>
</div>
break;
case PlaylistItem playlist:
<div class="playlist">
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="thumbnail" style="background-image: url('@playlist.Thumbnails.LastOrDefault()?.Url')">
<div>
<span>@playlist.VideoCount</span>
<span>VIDEOS</span>
</div>
</a>
<div class="info">
<a href="/watch?v=@playlist.FirstVideoId&list=@playlist.Id" class="title max-lines-2">@playlist.Title</a>
<div>
<a href="/channel/@playlist.Channel.Id">@playlist.Channel.Name</a>
</div>
</div>
</div>
break;
case RadioItem radio:
<div class="playlist">
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="thumbnail" style="background-image: url('@radio.Thumbnails.LastOrDefault()?.Url')">
<div>
<span>MIX</span>
</div>
</a>
<div class="info">
<a href="/watch?v=@radio.FirstVideoId&list=@radio.Id" class="title max-lines-2">@radio.Title</a>
<div>
<span>@radio.Channel.Name</span>
</div>
</div>
</div>
break;
case ContinuationItem continuationItem:
break;
default:
<div class="video">
<div class="thumbnail" style="background-image: url('@recommendation.Thumbnails?.LastOrDefault()?.Url')"></div>
<div class="info">
<span class="title max-lines-2">@recommendation.GetType().Name</span>
<div>
<b>WARNING:</b> Unknown recommendation type: @recommendation.Id
</div>
</div>
</div>
break;
}
}
</div>
</div>
</div>
@if (canPlay)
{
@if (Model.MobileLayout)
{
<script src="/js/lt-video/player-mobile.js"></script>
}
else
{
<script src="/js/lt-video/player-desktop.js"></script>
}
@if (!Model.CompatibilityMode && !live)
{
<script src="/js/shaka-player/shaka-player.compiled.min.js"></script>
<script>
let player = undefined;
loadPlayerWithShaka("video", {
"id": "@Model.Video.Id",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": false,
"live": false,
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
}, [
@foreach (Format f in Model.Player.Formats.Reverse())
{
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/video?url=@HttpUtility.UrlEncode(f.Url)"},
}
], "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).mpd").then(x => player = x).catch(alert);
</script>
}
else if (live)
{
<script src="/js/hls.js/hls.min.js"></script>
<script>
let player = undefined;
loadPlayerWithHls("video", {
"id": "@(Model.Video.Id)",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": false,
"live": true
}, "https://@(Context.Request.Host)/manifest/@(Model.Video.Id).m3u8").then(x => player = x).catch(alert);
</script>
}
else
{
<script>
const player = new Player("video", {
"id": "@Model.Video.Id",
"title": "@Html.Raw(Model.Video.Title.Replace("\"", "\\\""))",
"embed": false,
"live": false,
"storyboard": "/proxy/image?url=@HttpUtility.UrlEncode(Model.Player.Storyboards.FirstOrDefault())"
}, [
@foreach (Format f in Model.Player.Formats.Reverse())
{
@:{"height": @f.Resolution.Split("x")[1],"label":"@f.FormatName","src": "/proxy/media/@(Model.Player.Id)/@(f.FormatId)"},
}
]);
</script>
}
}

View File

@@ -0,0 +1,3 @@
@using LightTube
@using LightTube.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
namespace LightTube
{
public static class YoutubeRSS
{
private static HttpClient _httpClient = new();
public static async Task<ChannelFeed> GetChannelFeed(string channelId)
{
HttpResponseMessage response =
await _httpClient.GetAsync("https://www.youtube.com/feeds/videos.xml?channel_id=" + channelId);
if (!response.IsSuccessStatusCode)
throw response.StatusCode switch
{
HttpStatusCode.NotFound => new KeyNotFoundException($"Channel '{channelId}' does not exist"),
var _ => new Exception("Failed to fetch RSS feed for channel " + channelId)
};
ChannelFeed feed = new();
string xml = await response.Content.ReadAsStringAsync();
XDocument doc = XDocument.Parse(xml);
feed.Name = doc.Descendants().First(p => p.Name.LocalName == "title").Value;
feed.Id = doc.Descendants().First(p => p.Name.LocalName == "channelId").Value;
feed.Videos = doc.Descendants().Where(p => p.Name.LocalName == "entry").Select(x => new FeedVideo
{
Id = x.Descendants().First(p => p.Name.LocalName == "videoId").Value,
Title = x.Descendants().First(p => p.Name.LocalName == "title").Value,
Description = x.Descendants().First(p => p.Name.LocalName == "description").Value,
ViewCount = long.Parse(x.Descendants().First(p => p.Name.LocalName == "statistics").Attribute("views")?.Value ?? "-1"),
Thumbnail = x.Descendants().First(p => p.Name.LocalName == "thumbnail").Attribute("url")?.Value,
ChannelName = x.Descendants().First(p => p.Name.LocalName == "name").Value,
ChannelId = x.Descendants().First(p => p.Name.LocalName == "channelId").Value,
PublishedDate = DateTimeOffset.Parse(x.Descendants().First(p => p.Name.LocalName == "published").Value)
}).ToArray();
return feed;
}
public static async Task<FeedVideo[]> GetMultipleFeeds(IEnumerable<string> channelIds)
{
Task<ChannelFeed>[] feeds = channelIds.Select(YoutubeRSS.GetChannelFeed).ToArray();
await Task.WhenAll(feeds);
List<FeedVideo> videos = new();
foreach (ChannelFeed feed in feeds.Select(x => x.Result)) videos.AddRange(feed.Videos);
videos.Sort((a, b) => DateTimeOffset.Compare(b.PublishedDate, a.PublishedDate));
return videos.ToArray();
}
}
public class ChannelFeed
{
public string Name;
public string Id;
public FeedVideo[] Videos;
}
public class FeedVideo
{
public string Id;
public string Title;
public string Description;
public long ViewCount;
public string Thumbnail;
public string ChannelName;
public string ChannelId;
public DateTimeOffset PublishedDate;
public XmlElement GetXmlElement(XmlDocument doc)
{
XmlElement item = doc.CreateElement("Video");
item.SetAttribute("id", Id);
item.SetAttribute("views", ViewCount.ToString());
item.SetAttribute("uploadedAt", PublishedDate.ToUnixTimeSeconds().ToString());
XmlElement title = doc.CreateElement("Title");
title.InnerText = Title;
item.AppendChild(title);
XmlElement channel = doc.CreateElement("Channel");
channel.SetAttribute("id", ChannelId);
XmlElement channelTitle = doc.CreateElement("Name");
channelTitle.InnerText = ChannelName;
channel.AppendChild(channelTitle);
item.AppendChild(channel);
XmlElement thumbnail = doc.CreateElement("Thumbnail");
thumbnail.InnerText = Thumbnail;
item.AppendChild(thumbnail);
if (!string.IsNullOrWhiteSpace(Description))
{
XmlElement description = doc.CreateElement("Description");
description.InnerText = Description;
item.AppendChild(description);
}
return item;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
:root {
--text-primary: #fff;
--text-secondary: #808080;
--text-link: #3ea6ff;
--app-background: #181818;
--context-menu-background: #333;
--border-color: #444;
--item-hover-background: #373737;
--item-active-background: #383838;
--top-bar-background: #202020;
--guide-background: #212121;
--thumbnail-background: #252525;
--channel-info-background: #181818;
--channel-contents-background: #0f0f0f;
}

View File

@@ -0,0 +1,19 @@
:root {
--text-primary: #000;
--text-secondary: #606060;
--text-link: #3ea6ff;
--app-background: #f9f9f9;
--context-menu-background: #f2f2f2;
--border-color: #c5c5c5;
--item-hover-background: #f2f2f2;
--item-active-background: #E5E5E5;;
--top-bar-background: #FFF;
--guide-background: #FFF;
--thumbnail-background: #CCC;
--channel-info-background: #fff;
--channel-contents-background: #f9f9f9;
}

File diff suppressed because it is too large Load Diff

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