mirror of
https://github.com/Benexl/FastAnime.git
synced 2025-12-06 04:41:06 -08:00
Compare commits
814 Commits
v2.0.0
...
feature/an
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f6ccc91e | ||
|
|
114fba27fd | ||
|
|
8b123cdae2 | ||
|
|
b91129c4a7 | ||
|
|
de0c656bc6 | ||
|
|
de774a58d2 | ||
|
|
ee25cbba10 | ||
|
|
278a771f64 | ||
|
|
0d8c287e2f | ||
|
|
74308dfdc5 | ||
|
|
7ca1b8572e | ||
|
|
54aed9e5a0 | ||
|
|
4511d14e8b | ||
|
|
bff684e8cb | ||
|
|
cfc83450c8 | ||
|
|
04a6a425b7 | ||
|
|
088d232bfd | ||
|
|
03fd8c0bf8 | ||
|
|
17f1744025 | ||
|
|
9a5f3d46be | ||
|
|
66eb854da5 | ||
|
|
ae62adf233 | ||
|
|
55a7c7facf | ||
|
|
2340c34d02 | ||
|
|
40b29ba6e5 | ||
|
|
5dc768f7e8 | ||
|
|
b343bfb645 | ||
|
|
37773265ce | ||
|
|
70ef1bf633 | ||
|
|
bee97acd35 | ||
|
|
fb61fd17f1 | ||
|
|
98fff7d00f | ||
|
|
3cc9ae50b6 | ||
|
|
26f7de172a | ||
|
|
673b6280e4 | ||
|
|
7943dcc3db | ||
|
|
49ee1f9bbd | ||
|
|
fd80149e74 | ||
|
|
7c11616bea | ||
|
|
b9130018ca | ||
|
|
70ade13017 | ||
|
|
071c0daf4f | ||
|
|
ed136fc8a0 | ||
|
|
3a29127366 | ||
|
|
b7c938fec4 | ||
|
|
c6aada6139 | ||
|
|
e4c4203364 | ||
|
|
52cd4a8d85 | ||
|
|
b9b0d49530 | ||
|
|
ba4df96dc8 | ||
|
|
335ba86367 | ||
|
|
92c18f850f | ||
|
|
70763807de | ||
|
|
51438c8864 | ||
|
|
03426bd0da | ||
|
|
ccdb0346eb | ||
|
|
6fba74b3ca | ||
|
|
b436f23f65 | ||
|
|
cd7b70dd6b | ||
|
|
ac9b000ce8 | ||
|
|
84a3b6185b | ||
|
|
7f52d8cb39 | ||
|
|
87372e41be | ||
|
|
9cfbc0bdcf | ||
|
|
af6e64ee0c | ||
|
|
4a2f272e14 | ||
|
|
a2e2ae8dd3 | ||
|
|
5ce9bbaa0d | ||
|
|
25812b6562 | ||
|
|
ee52b945ea | ||
|
|
be14e6a135 | ||
|
|
9402e7a2b6 | ||
|
|
590d6a1851 | ||
|
|
8186fe9991 | ||
|
|
4d2831eee1 | ||
|
|
ea918909b9 | ||
|
|
93c0f2ab83 | ||
|
|
985e7fee18 | ||
|
|
7bd7ddecae | ||
|
|
9afb9a9a32 | ||
|
|
40065478cc | ||
|
|
65aa8fcb4e | ||
|
|
2717d0b012 | ||
|
|
9f0cf5f8dc | ||
|
|
ef4a850d75 | ||
|
|
d8804c711e | ||
|
|
adf75f65b2 | ||
|
|
6e7e75b514 | ||
|
|
9515559afb | ||
|
|
e8849940e1 | ||
|
|
007954802f | ||
|
|
b874bef2d5 | ||
|
|
6c1bcebd99 | ||
|
|
fe5e8c641d | ||
|
|
45a4913ead | ||
|
|
03f0f40c9a | ||
|
|
af616e0047 | ||
|
|
6000ed2a84 | ||
|
|
5a869060d8 | ||
|
|
58618bd82d | ||
|
|
84570c5595 | ||
|
|
18b4071ad9 | ||
|
|
fd052c87de | ||
|
|
ee959b3428 | ||
|
|
ad6bdad594 | ||
|
|
316832e771 | ||
|
|
9edeeb5ca4 | ||
|
|
7eb6054d5c | ||
|
|
5b06039cef | ||
|
|
abe36296c1 | ||
|
|
dcbf0df1a0 | ||
|
|
c2acbcdb68 | ||
|
|
96233b14ff | ||
|
|
a8f2579f82 | ||
|
|
5ed9700c5c | ||
|
|
fd74fbe2ef | ||
|
|
19426019a2 | ||
|
|
276c8d48d9 | ||
|
|
99809f3fd3 | ||
|
|
f79c8540c3 | ||
|
|
e602a6fbc4 | ||
|
|
8abfaed7bf | ||
|
|
15b920698a | ||
|
|
44cf9c3da7 | ||
|
|
460d3c7d94 | ||
|
|
5f030a5d9e | ||
|
|
8091e23196 | ||
|
|
e641a48156 | ||
|
|
c59babc30d | ||
|
|
494104ee19 | ||
|
|
159136cfb1 | ||
|
|
7e211f109e | ||
|
|
48e906e464 | ||
|
|
98c2fef8cd | ||
|
|
7088b8ce18 | ||
|
|
6cfc766db3 | ||
|
|
89ff453778 | ||
|
|
1c95d45be4 | ||
|
|
75e67c22d2 | ||
|
|
1a1d8cc8f4 | ||
|
|
3ea37c4079 | ||
|
|
b18e419831 | ||
|
|
fe06c8e0f1 | ||
|
|
759276237f | ||
|
|
d475dda41c | ||
|
|
ad499657e0 | ||
|
|
dbf96afea7 | ||
|
|
001a63d3df | ||
|
|
1207426a96 | ||
|
|
82ca5f32b1 | ||
|
|
2924fcd077 | ||
|
|
9fc66db248 | ||
|
|
f4e73c3335 | ||
|
|
5246a2fc4b | ||
|
|
4bbfe221f2 | ||
|
|
6017833605 | ||
|
|
f2538f5341 | ||
|
|
fec09e9b74 | ||
|
|
67a174158d | ||
|
|
ae3a59d116 | ||
|
|
fd59d64b76 | ||
|
|
b1ac4a6558 | ||
|
|
8e9aeb660f | ||
|
|
4f401aa91c | ||
|
|
b8733eccbd | ||
|
|
3617465f64 | ||
|
|
4c9ecafb9b | ||
|
|
e87e4e5639 | ||
|
|
16fa39397d | ||
|
|
24ffc4ee53 | ||
|
|
3e0be026eb | ||
|
|
a04643d36a | ||
|
|
5392d4f25a | ||
|
|
63e107ba53 | ||
|
|
9ba3f88813 | ||
|
|
5da5dc5dcc | ||
|
|
3c42f660ce | ||
|
|
4349b9fc22 | ||
|
|
048d008ca1 | ||
|
|
17636d766a | ||
|
|
367520450d | ||
|
|
d6f773f41f | ||
|
|
f76350bc5b | ||
|
|
d299355d90 | ||
|
|
4aa9fa9253 | ||
|
|
fcb16a574e | ||
|
|
8b52a1ef27 | ||
|
|
574a739cb6 | ||
|
|
f27a98aaa6 | ||
|
|
a266a7100f | ||
|
|
d1e07930f9 | ||
|
|
bc7936d9cc | ||
|
|
19c6656cdf | ||
|
|
06506fb47f | ||
|
|
29480c64cd | ||
|
|
474da2f1fd | ||
|
|
abac604ccd | ||
|
|
48f46cdf3d | ||
|
|
9cafcde9e1 | ||
|
|
e908c793c6 | ||
|
|
0fd69d03dd | ||
|
|
3a9be3f699 | ||
|
|
d3f08ea9c4 | ||
|
|
9efe9f9949 | ||
|
|
83933f7a63 | ||
|
|
afe1cb68f6 | ||
|
|
a6ddb10734 | ||
|
|
f678fa13f0 | ||
|
|
2067467134 | ||
|
|
d78b62fcee | ||
|
|
6c30cf808b | ||
|
|
6e9babf270 | ||
|
|
aa50ab62b5 | ||
|
|
88975e59c0 | ||
|
|
64c427fe41 | ||
|
|
987ae57e33 | ||
|
|
ac36e24a32 | ||
|
|
04d877a72e | ||
|
|
43174db8e4 | ||
|
|
3092ef0887 | ||
|
|
5e45fba66d | ||
|
|
65e4726f82 | ||
|
|
384d326fa8 | ||
|
|
60c583d115 | ||
|
|
f716f9687a | ||
|
|
db1006a6b2 | ||
|
|
9163b1394d | ||
|
|
0ce27f8e50 | ||
|
|
0e6aeeea18 | ||
|
|
452c2cf764 | ||
|
|
a1de0548f4 | ||
|
|
f60cdea2e1 | ||
|
|
b67284cfeb | ||
|
|
17161f5f78 | ||
|
|
c0d87c4351 | ||
|
|
725fe4875d | ||
|
|
ac3c6801d7 | ||
|
|
27b1f3f792 | ||
|
|
49cdd440df | ||
|
|
490f8b0e8b | ||
|
|
5dde02570a | ||
|
|
e3deb28d26 | ||
|
|
1a85b2f216 | ||
|
|
0639a3c949 | ||
|
|
bdbf0821c5 | ||
|
|
5e81c44312 | ||
|
|
41ed56f395 | ||
|
|
26f9c3b8de | ||
|
|
ecdd1b5f20 | ||
|
|
b6dd965e49 | ||
|
|
273dd56680 | ||
|
|
be4cc58e47 | ||
|
|
c882691412 | ||
|
|
f4c4c874df | ||
|
|
f8992d46dd | ||
|
|
222c50b4b2 | ||
|
|
064401f8e8 | ||
|
|
a079f9919c | ||
|
|
a88df7f3ef | ||
|
|
d1dfddf290 | ||
|
|
e8491e3723 | ||
|
|
2f21e7139b | ||
|
|
f5c831077d | ||
|
|
badd10bf97 | ||
|
|
42bd4963b8 | ||
|
|
f08ff7155c | ||
|
|
e487435d7e | ||
|
|
54f7327ed7 | ||
|
|
ba620bae96 | ||
|
|
48eac48738 | ||
|
|
194b8ca2df | ||
|
|
96c2d4976c | ||
|
|
c5034a5829 | ||
|
|
7c91288e6e | ||
|
|
de2ba342ad | ||
|
|
b847e02fe0 | ||
|
|
f02f92b80b | ||
|
|
a2da6974fa | ||
|
|
be1babbedc | ||
|
|
5c804f7aa6 | ||
|
|
723a7ab24f | ||
|
|
18a9b07144 | ||
|
|
d279cc70b9 | ||
|
|
27d71cbb23 | ||
|
|
615b420c74 | ||
|
|
f9c2b6e939 | ||
|
|
85368393fc | ||
|
|
b9636c94d3 | ||
|
|
4920ee508a | ||
|
|
fd448ad701 | ||
|
|
b223a34879 | ||
|
|
d5e1e60266 | ||
|
|
783b63219f | ||
|
|
317fee916b | ||
|
|
870bb24e1b | ||
|
|
f51ceaacd7 | ||
|
|
cdad70e40d | ||
|
|
0737c5c14b | ||
|
|
32f4d9271f | ||
|
|
355f10dd9e | ||
|
|
2f2ffc0a84 | ||
|
|
e35683e90a | ||
|
|
2bd02c7e99 | ||
|
|
5a50e79216 | ||
|
|
ec78c81381 | ||
|
|
f042e5042b | ||
|
|
428bbb20bd | ||
|
|
3af31a2dfd | ||
|
|
759889acd4 | ||
|
|
d106bf7c5d | ||
|
|
00ff89d14f | ||
|
|
76460b6c54 | ||
|
|
e58fd33fe0 | ||
|
|
931f9f10f8 | ||
|
|
0e9dbd2c6b | ||
|
|
3bdfa27e1c | ||
|
|
f46f09ffdf | ||
|
|
d309c04214 | ||
|
|
b19a323d15 | ||
|
|
ff94edfd05 | ||
|
|
59e1a82646 | ||
|
|
2e902fa4e7 | ||
|
|
d2e17af7a9 | ||
|
|
f1fa40c419 | ||
|
|
8bbde97403 | ||
|
|
a2b7d71eb2 | ||
|
|
67b4f0ea38 | ||
|
|
a6d5d5f37c | ||
|
|
67a066f16e | ||
|
|
e6297619d4 | ||
|
|
8f514858f2 | ||
|
|
eb9cffbd7a | ||
|
|
c5f9c37d3a | ||
|
|
44fd65ebab | ||
|
|
e919980ff7 | ||
|
|
6887c6ff10 | ||
|
|
b394de0b23 | ||
|
|
71003049d6 | ||
|
|
6f69b785d8 | ||
|
|
6756540fd1 | ||
|
|
e6b9df25dd | ||
|
|
f40dd2363a | ||
|
|
61a525ff94 | ||
|
|
27d671f89d | ||
|
|
58edb0427f | ||
|
|
7abcdc8f7c | ||
|
|
ca4ca0d476 | ||
|
|
5a337b1c97 | ||
|
|
6c94dd22fc | ||
|
|
8c7e1e201f | ||
|
|
98e41e1eb5 | ||
|
|
bd0e7db73c | ||
|
|
fb705b4ac2 | ||
|
|
93654be74f | ||
|
|
9b14a4c723 | ||
|
|
bce5acf7b5 | ||
|
|
228be7e1f7 | ||
|
|
2eb434e42a | ||
|
|
fbb3a00ab0 | ||
|
|
8341ffe8fd | ||
|
|
0822e2e92c | ||
|
|
3b9fbd0665 | ||
|
|
60b74bee18 | ||
|
|
d7dc63e003 | ||
|
|
d40edb6ff6 | ||
|
|
803712649f | ||
|
|
7bc0d33f69 | ||
|
|
5885d134df | ||
|
|
5500ec49c8 | ||
|
|
8c94380050 | ||
|
|
87a97dd0c6 | ||
|
|
80d9f732b1 | ||
|
|
051273dac9 | ||
|
|
036f448906 | ||
|
|
b5aeed9268 | ||
|
|
4257502b85 | ||
|
|
28a857520f | ||
|
|
4f9fff375c | ||
|
|
ce31f63788 | ||
|
|
9412c2491e | ||
|
|
8209adec62 | ||
|
|
39703d9eca | ||
|
|
57d16b3e18 | ||
|
|
73a99f8b96 | ||
|
|
309d7d5858 | ||
|
|
8d20e490ca | ||
|
|
3a6e005f3a | ||
|
|
bdf49bd7ce | ||
|
|
c4df2587d0 | ||
|
|
b38f66767f | ||
|
|
6c0e0ccf72 | ||
|
|
e39c992883 | ||
|
|
a1744fc9b3 | ||
|
|
3c5106c32c | ||
|
|
fd0d899f72 | ||
|
|
c753873f61 | ||
|
|
4c8ff2ae9b | ||
|
|
23274de367 | ||
|
|
2aec40ead0 | ||
|
|
172f2bb1de | ||
|
|
2f5684a93a | ||
|
|
1d40160abf | ||
|
|
af84d80137 | ||
|
|
e6412631ae | ||
|
|
978d8d45ba | ||
|
|
06575120d6 | ||
|
|
72cec28613 | ||
|
|
8023edcf3a | ||
|
|
0cb50cd506 | ||
|
|
9981b3dec8 | ||
|
|
50c048e158 | ||
|
|
c0a57c7814 | ||
|
|
bcdd88c725 | ||
|
|
d45d438663 | ||
|
|
3d12059e27 | ||
|
|
677f4690fa | ||
|
|
a79b59f727 | ||
|
|
5641c245e7 | ||
|
|
058fc285cd | ||
|
|
71cfe667c9 | ||
|
|
d9692201aa | ||
|
|
1fd4087b41 | ||
|
|
787eb0c9ca | ||
|
|
acd937f8ab | ||
|
|
52af68d13f | ||
|
|
1ff3074fad | ||
|
|
debaa2ffa6 | ||
|
|
5b6ccbe748 | ||
|
|
d6ca923951 | ||
|
|
0e9bf7f2de | ||
|
|
ccad2435b0 | ||
|
|
30fa9851dd | ||
|
|
000bae9bb7 | ||
|
|
8c2bb71e08 | ||
|
|
57393b085a | ||
|
|
5f721847d7 | ||
|
|
383cb62ede | ||
|
|
434ac947dd | ||
|
|
d0fb39cede | ||
|
|
f98ae77587 | ||
|
|
33e1b0fb6f | ||
|
|
7134702eb9 | ||
|
|
cac7586a86 | ||
|
|
0b9da27def | ||
|
|
ddbb4ca451 | ||
|
|
757393aa36 | ||
|
|
eb54d5e995 | ||
|
|
0d95a38321 | ||
|
|
8d2734db74 | ||
|
|
b3abcb958b | ||
|
|
0667749e4c | ||
|
|
57e73e6799 | ||
|
|
7d890b9719 | ||
|
|
8cbbcf458d | ||
|
|
67bc25a527 | ||
|
|
e668f9326a | ||
|
|
a02db6471f | ||
|
|
08b1f0c90c | ||
|
|
3ec8dbee8c | ||
|
|
473c11faca | ||
|
|
320e3799d3 | ||
|
|
a0f28ddf6d | ||
|
|
9512c3530a | ||
|
|
72602a0ec1 | ||
|
|
4daf6a2b07 | ||
|
|
8b37927f6a | ||
|
|
9d6f785a7f | ||
|
|
897c34d98c | ||
|
|
28c75215bd | ||
|
|
8697b27fe0 | ||
|
|
b6e05c877b | ||
|
|
d8c3ba6181 | ||
|
|
8b5c917038 | ||
|
|
856f62c245 | ||
|
|
02dfc9d71c | ||
|
|
cef0bae528 | ||
|
|
4867720ad2 | ||
|
|
8d85e30150 | ||
|
|
eb99b7e6ba | ||
|
|
089c049f26 | ||
|
|
a33e47d205 | ||
|
|
25dc35eaaf | ||
|
|
525586e955 | ||
|
|
5129219e23 | ||
|
|
7cd97c78b1 | ||
|
|
27b4422ef3 | ||
|
|
1c367c8aa1 | ||
|
|
7b6cc48b90 | ||
|
|
812d0110a7 | ||
|
|
60b05bf0ac | ||
|
|
d830cca3bc | ||
|
|
209e93b6d9 | ||
|
|
b10d9dc39a | ||
|
|
fe8cda094c | ||
|
|
33c06eab0a | ||
|
|
f3f4be7410 | ||
|
|
3915ef0fb6 | ||
|
|
20d26166dd | ||
|
|
ddca724bd8 | ||
|
|
b86c1a0479 | ||
|
|
1fa7830ddf | ||
|
|
59abafbe16 | ||
|
|
b6eebb9736 | ||
|
|
7797053102 | ||
|
|
d763445f72 | ||
|
|
7bc6b14b5f | ||
|
|
f70d2ac8af | ||
|
|
defdfc5a47 | ||
|
|
e67eeda492 | ||
|
|
a17588d02c | ||
|
|
67b59305c4 | ||
|
|
61db9aeea6 | ||
|
|
4f0768a060 | ||
|
|
21704cbbea | ||
|
|
886bc4d011 | ||
|
|
e3437e066a | ||
|
|
8f2795843a | ||
|
|
c6290592e8 | ||
|
|
050ba740b8 | ||
|
|
0b1a27b223 | ||
|
|
bafd04b788 | ||
|
|
fb5f51eea5 | ||
|
|
799e1f0681 | ||
|
|
53a2d953f8 | ||
|
|
9ce5bc3c76 | ||
|
|
dc58fc8536 | ||
|
|
1d5c3016fc | ||
|
|
8737aea746 | ||
|
|
bd03866f5e | ||
|
|
81690a8015 | ||
|
|
933112a52b | ||
|
|
eb513dfe0e | ||
|
|
3928b77506 | ||
|
|
95cb2bd78c | ||
|
|
4fa1c45eb2 | ||
|
|
b9051bc792 | ||
|
|
a590024f1c | ||
|
|
2f51936679 | ||
|
|
327c50d290 | ||
|
|
031dfbb9b5 | ||
|
|
050365302a | ||
|
|
0f248b1119 | ||
|
|
871d5cf758 | ||
|
|
320376d2e8 | ||
|
|
02e7fdff6f | ||
|
|
2c5c28f295 | ||
|
|
2d3509ccc1 | ||
|
|
30babf2d69 | ||
|
|
cfbbabf898 | ||
|
|
5ac6c45fdf | ||
|
|
a14645b563 | ||
|
|
90dbc26c46 | ||
|
|
54cc830c35 | ||
|
|
4928ff5b74 | ||
|
|
bb481fe21a | ||
|
|
0d27b8f652 | ||
|
|
bdd3aae399 | ||
|
|
af94cd7eb5 | ||
|
|
54044f9527 | ||
|
|
1e5c039ece | ||
|
|
15555759dc | ||
|
|
0ed51e05cc | ||
|
|
634ef6febf | ||
|
|
bda4b2dbe1 | ||
|
|
f015305e7c | ||
|
|
d32b7e917f | ||
|
|
3b35e80199 | ||
|
|
c65a1a2815 | ||
|
|
0b3615c9f5 | ||
|
|
3ac4e1ac71 | ||
|
|
d62f580d7a | ||
|
|
02e35b66cb | ||
|
|
7b11e0a301 | ||
|
|
aa8b91aed3 | ||
|
|
fe0fa97576 | ||
|
|
92059cd5ed | ||
|
|
ed3064e3b1 | ||
|
|
441d1e5e6c | ||
|
|
653b2cf4eb | ||
|
|
8d4b71e0c8 | ||
|
|
29cc6cad09 | ||
|
|
8119eef263 | ||
|
|
912c8674cf | ||
|
|
6b3ca236dd | ||
|
|
f1c352d4ff | ||
|
|
714533d845 | ||
|
|
56dd25df8d | ||
|
|
8248dc53df | ||
|
|
1a8a187de6 | ||
|
|
bc86be8c93 | ||
|
|
75026d4fc5 | ||
|
|
f8a5ccb8d2 | ||
|
|
719d1bd187 | ||
|
|
0dd83463c6 | ||
|
|
966301bce8 | ||
|
|
d776880306 | ||
|
|
1ee50e8a55 | ||
|
|
ae95c5ea3d | ||
|
|
d64ad5e11d | ||
|
|
d1a47c6d44 | ||
|
|
51a834a62f | ||
|
|
3a030bf6f7 | ||
|
|
eb6a6fc82c | ||
|
|
437ccd94e4 | ||
|
|
d65868cc30 | ||
|
|
8678aa6544 | ||
|
|
00e5141152 | ||
|
|
90e757dfe1 | ||
|
|
8b471b08e8 | ||
|
|
158bc5710f | ||
|
|
a0b946a13d | ||
|
|
b547b75f03 | ||
|
|
58c7427a47 | ||
|
|
6220b9c55d | ||
|
|
6b9b5c131c | ||
|
|
212f2af39c | ||
|
|
f7b2b4e0c9 | ||
|
|
a747529279 | ||
|
|
1dfdcc27ce | ||
|
|
3c03289453 | ||
|
|
06fd446a72 | ||
|
|
172d912d8b | ||
|
|
2396018607 | ||
|
|
a9be9779c5 | ||
|
|
2f76b26a99 | ||
|
|
2fe5edf810 | ||
|
|
d67ee6a779 | ||
|
|
e06ec5dbd4 | ||
|
|
c1b24ba2aa | ||
|
|
59e9cf9fd0 | ||
|
|
58761f5b96 | ||
|
|
ac959da229 | ||
|
|
bacc8c48ec | ||
|
|
905a159428 | ||
|
|
20f734cab2 | ||
|
|
7c2c644aef | ||
|
|
0efc92081a | ||
|
|
fafeee2367 | ||
|
|
e03063cd76 | ||
|
|
93b38b055f | ||
|
|
045635fb55 | ||
|
|
de7f773e9e | ||
|
|
ef6a465bd2 | ||
|
|
0c623af8a4 | ||
|
|
0589f83998 | ||
|
|
e17608afd5 | ||
|
|
b915654685 | ||
|
|
2ce9bf6c47 | ||
|
|
3c22232432 | ||
|
|
3474e9520c | ||
|
|
e9bacf4f9c | ||
|
|
ef422ed6fd | ||
|
|
d0f5366908 | ||
|
|
3557205feb | ||
|
|
ba4c41d888 | ||
|
|
1427a3193c | ||
|
|
b5cee20e56 | ||
|
|
be7f464073 | ||
|
|
c7f8f168f5 | ||
|
|
ba59fbdcb0 | ||
|
|
9f54fa4998 | ||
|
|
3c9688b32c | ||
|
|
1f046447bb | ||
|
|
87e3a275bb | ||
|
|
037b5c36a4 | ||
|
|
7d8b60fb14 | ||
|
|
0ad16fee53 | ||
|
|
249243aeb4 | ||
|
|
c208dc3579 | ||
|
|
ea93f2ba23 | ||
|
|
d910a0bb6a | ||
|
|
550fcfeddc | ||
|
|
c6910e5a1c | ||
|
|
8555edb521 | ||
|
|
139193ce29 | ||
|
|
1a87375ccd | ||
|
|
83cbef40f6 | ||
|
|
85b4fc75a1 | ||
|
|
f2e2da378f | ||
|
|
7c34bc9120 | ||
|
|
6f153f2acb | ||
|
|
8171083978 | ||
|
|
db5b9a59b4 | ||
|
|
6fa656ba11 | ||
|
|
de0682c1bb | ||
|
|
a6a32d8de4 | ||
|
|
bb14b269de | ||
|
|
14331d8bc2 | ||
|
|
1729464844 | ||
|
|
5fb9747285 | ||
|
|
394228d391 | ||
|
|
5d3c0cc6ec | ||
|
|
3ef7c5248c | ||
|
|
8bebc401fd | ||
|
|
215b28457b | ||
|
|
dfd2bfc857 | ||
|
|
f991292e94 | ||
|
|
d837457f80 | ||
|
|
343bdba31b | ||
|
|
1c1c2457e8 | ||
|
|
b083bfb074 | ||
|
|
ea1abcb2ae | ||
|
|
001030ba2b | ||
|
|
eda8984781 | ||
|
|
d8dc6f0a34 | ||
|
|
2d711a7a7f | ||
|
|
30ca25626a | ||
|
|
b1f5a558c8 | ||
|
|
8062c8dc83 | ||
|
|
cb7eed46bc | ||
|
|
4626eca89e | ||
|
|
0d549c5915 | ||
|
|
33c518ed4c | ||
|
|
8e155dcc74 | ||
|
|
7743b0423e | ||
|
|
6346ea7343 | ||
|
|
32de01047f | ||
|
|
35c7f81afb | ||
|
|
2dbbb1c4df | ||
|
|
6a6efa9d56 | ||
|
|
e510dc3a11 | ||
|
|
9639fd8c05 | ||
|
|
add35ce682 | ||
|
|
6bcc77ea44 | ||
|
|
1a72f88be3 | ||
|
|
1a9f1120b8 | ||
|
|
c2fc807688 | ||
|
|
2b0ade093c | ||
|
|
a26193706e | ||
|
|
ff3c57ef9b | ||
|
|
3b987bd07a | ||
|
|
e8474c0428 | ||
|
|
c78a759aa1 | ||
|
|
d1aad70c48 | ||
|
|
62b36f3e58 | ||
|
|
c5b905fb0d | ||
|
|
7d3dc671ed | ||
|
|
0ec3c7a5bb | ||
|
|
8e0619863a | ||
|
|
e8a05ec4b8 | ||
|
|
34e8b2abd1 | ||
|
|
161b6eb961 | ||
|
|
dd2090f85d | ||
|
|
8b1595a5da | ||
|
|
77ffa27ed8 | ||
|
|
15f79b65c9 | ||
|
|
33c3af0241 | ||
|
|
9badde62fb | ||
|
|
4e401dca40 | ||
|
|
25422b1b7d | ||
|
|
e8463f13b4 | ||
|
|
556f42e41f | ||
|
|
b99a4f7efc | ||
|
|
f6f45cf322 | ||
|
|
ae6db1847a | ||
|
|
20d04ea07b | ||
|
|
8f3834453c | ||
|
|
7ad8b8a0e3 | ||
|
|
80b41f06da | ||
|
|
e79321ed50 | ||
|
|
f7b5898dfa | ||
|
|
144bf53081 | ||
|
|
16dded9724 | ||
|
|
c47b158bff | ||
|
|
9a36e15d9d | ||
|
|
d6b2bd7761 | ||
|
|
2346552dc4 | ||
|
|
ba275055db | ||
|
|
de4ddf2f3a | ||
|
|
9c94d824d1 | ||
|
|
495f3cfbf6 | ||
|
|
b56c9ae3dd | ||
|
|
5e9ef87526 | ||
|
|
b68d6d6fe9 | ||
|
|
5870cc6640 | ||
|
|
7a43d58d82 | ||
|
|
fc7efebc8d | ||
|
|
528be74194 | ||
|
|
ab782acf2f | ||
|
|
45836d1ebc | ||
|
|
dff059d8eb | ||
|
|
4010cfc9c8 | ||
|
|
6329730820 | ||
|
|
006592ae7d | ||
|
|
831dcf4e88 | ||
|
|
0d2cf7ed66 | ||
|
|
aa6dc2b98e | ||
|
|
2e5cde3365 | ||
|
|
d75a03e594 | ||
|
|
9268c02683 | ||
|
|
89913036c9 | ||
|
|
2244026c67 | ||
|
|
c70564474b | ||
|
|
74514c9fbc | ||
|
|
077e9ab8c4 | ||
|
|
b05f7f1640 | ||
|
|
3382b720e3 | ||
|
|
f72c2d4b17 | ||
|
|
ff027991e0 | ||
|
|
21cdc6b015 | ||
|
|
29a2e3e6d1 | ||
|
|
5b3b9f740b | ||
|
|
5bc0e52179 | ||
|
|
40f1c4fba5 | ||
|
|
454341eaf5 | ||
|
|
abab2540a3 | ||
|
|
b2bc8cbace | ||
|
|
90bbf3c033 | ||
|
|
ac91b1770a | ||
|
|
19d42b7924 | ||
|
|
9ec3136734 | ||
|
|
943fca43cf | ||
|
|
b2e00feb94 | ||
|
|
f726c8d55c |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: benexl # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: benexl # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
30
.github/chatmodes/new-command.chatmode.md
vendored
Normal file
30
.github/chatmodes/new-command.chatmode.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
description: "Generate a new 'click' command following the project's lazy-loading pattern and service architecture."
|
||||
tools: ['codebase']
|
||||
---
|
||||
# FastAnime: CLI Command Generation Mode
|
||||
|
||||
You are an expert on the `fastanime` CLI structure, which uses `click` and a custom `LazyGroup` for performance. Your task is to generate the boilerplate for a new command.
|
||||
|
||||
**First, ask the user if this is a top-level command (like `fastanime new-cmd`) or a subcommand (like `fastanime anilist new-sub-cmd`).**
|
||||
|
||||
---
|
||||
|
||||
### If Top-Level Command:
|
||||
|
||||
1. **File Location:** State that the new command file should be created at: `fastanime/cli/commands/{command_name}.py`.
|
||||
2. **Boilerplate:** Generate the `click.command()` function.
|
||||
* It **must** accept `config: AppConfig` as the first argument using `@click.pass_obj`.
|
||||
* It **must not** contain business logic. Instead, show how to instantiate a service from `fastanime.cli.service` and call its methods.
|
||||
3. **Registration:** Instruct the user to register the command by adding it to the `commands` dictionary in `fastanime/cli/cli.py`. Provide the exact line to add, like: `"new-cmd": "new_cmd.new_cmd_function"`.
|
||||
|
||||
---
|
||||
|
||||
### If Subcommand:
|
||||
|
||||
1. **Ask for Parent:** Ask for the parent command group (e.g., `anilist`, `registry`).
|
||||
2. **File Location:** State that the new command file should be created at: `fastanime/cli/commands/{parent_name}/commands/{command_name}.py`.
|
||||
3. **Boilerplate:** Generate the `click.command()` function, similar to the top-level command.
|
||||
4. **Registration:** Instruct the user to register the subcommand in the parent's `cmd.py` file (e.g., `fastanime/cli/commands/anilist/cmd.py`) by adding it to the `lazy_subcommands` dictionary within the `@click.group` decorator.
|
||||
|
||||
**Final Instruction:** Remind the user that if the command introduces new logic, it should be encapsulated in a new or existing **Service** class in the `fastanime/cli/service/` directory. The CLI command function should only handle argument parsing and calling the service.
|
||||
34
.github/chatmodes/new-component.chatmode.md
vendored
Normal file
34
.github/chatmodes/new-component.chatmode.md
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
description: "Scaffold the necessary files and code for a new Player or Selector component, including configuration."
|
||||
tools: ['codebase', 'search']
|
||||
---
|
||||
# FastAnime: New Component Generation Mode
|
||||
|
||||
You are an expert on `fastanime`'s modular architecture. Your task is to help the developer add a new **Player** or **Selector** component.
|
||||
|
||||
**First, ask the user whether they want to create a 'Player' or a 'Selector'.** Then, follow the appropriate path below.
|
||||
|
||||
---
|
||||
|
||||
### If the user chooses 'Player':
|
||||
|
||||
1. **Scaffold Directory:** Create a directory at `fastanime/libs/player/{player_name}/`.
|
||||
2. **Implement `BasePlayer`:** Create a `player.py` file with a class `NewPlayer` that inherits from `fastanime.libs.player.base.BasePlayer`. Implement the `play` and `play_with_ipc` methods. The `play` method should use `subprocess` to call the player's executable.
|
||||
3. **Add Configuration:**
|
||||
* Instruct to create a new Pydantic model `NewPlayerConfig(OtherConfig)` in `fastanime/core/config/model.py`.
|
||||
* Add the new config model to the main `AppConfig`.
|
||||
* Add defaults in `fastanime/core/config/defaults.py` and descriptions in `fastanime/core/config/descriptions.py`.
|
||||
4. **Register Player:** Instruct to modify `fastanime/libs/player/player.py` by:
|
||||
* Adding the player name to the `PLAYERS` list.
|
||||
* Adding the instantiation logic to the `PlayerFactory.create` method.
|
||||
|
||||
---
|
||||
|
||||
### If the user chooses 'Selector':
|
||||
|
||||
1. **Scaffold Directory:** Create a directory at `fastanime/libs/selectors/{selector_name}/`.
|
||||
2. **Implement `BaseSelector`:** Create a `selector.py` file with a class `NewSelector` that inherits from `fastanime.libs.selectors.base.BaseSelector`. Implement the `choose`, `confirm`, and `ask` methods.
|
||||
3. **Add Configuration:** (Follow the same steps as for a Player).
|
||||
4. **Register Selector:**
|
||||
* Instruct to modify `fastanime/libs/selectors/selector.py` by adding the selector name to the `SELECTORS` list and the factory logic to `SelectorFactory.create`.
|
||||
* Instruct to update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).
|
||||
27
.github/chatmodes/new-provider.chatmode.md
vendored
Normal file
27
.github/chatmodes/new-provider.chatmode.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: "Scaffold and implement a new anime provider, following all architectural patterns of the fastanime project."
|
||||
tools: ['codebase', 'search', 'fetch']
|
||||
---
|
||||
# FastAnime: New Provider Generation Mode
|
||||
|
||||
You are an expert on the `fastanime` codebase, specializing in its provider architecture. Your task is to guide the developer in creating a new anime provider. You must strictly adhere to the project's structure and coding conventions.
|
||||
|
||||
**Your process is as follows:**
|
||||
|
||||
1. **Ask for the Provider's Name:** First, ask the user for the name of the new provider (e.g., `gogoanime`, `crunchyroll`). Use this name (in lowercase) for all subsequent file and directory naming.
|
||||
|
||||
2. **Scaffold the Directory Structure:** Based on the name, state the required directory structure that needs to be created:
|
||||
`fastanime/libs/provider/anime/{provider_name}/`
|
||||
|
||||
3. **Scaffold the Core Files:** Generate the initial code for the following files inside the new directory. Ensure all code is fully type-hinted.
|
||||
|
||||
* **`__init__.py`**: Can be an empty file.
|
||||
* **`types.py`**: Create placeholder `TypedDict` models for the provider's specific API responses (e.g., `GogoAnimeSearchResult`, `GogoAnimeEpisode`).
|
||||
* **`mappers.py`**: Create empty mapping functions that will convert the provider-specific types into the generic types from `fastanime.libs.provider.anime.types`. For example: `map_to_search_results(data: GogoAnimeSearchPage) -> SearchResults:`.
|
||||
* **`provider.py`**: Generate the main provider class. It **MUST** inherit from `fastanime.libs.provider.anime.base.BaseAnimeProvider`. Include stubs for the required abstract methods: `search`, `get`, and `episode_streams`. Remind the user to use `httpx.Client` for requests and to call the mapper functions.
|
||||
|
||||
4. **Instruct on Registration:** Clearly state the two files that **must** be modified to register the new provider:
|
||||
* **`fastanime/libs/provider/anime/types.py`**: Add the new provider's name to the `ProviderName` enum.
|
||||
* **`fastanime/libs/provider/anime/provider.py`**: Add an entry to the `PROVIDERS_AVAILABLE` dictionary.
|
||||
|
||||
5. **Final Guidance:** Remind the developer to add any title normalization rules to `fastanime/assets/normalizer.json` if the provider uses different anime titles than AniList.
|
||||
73
.github/chatmodes/plan.chatmode.md
vendored
Normal file
73
.github/chatmodes/plan.chatmode.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
description: "Plan new features or bug fixes with architectural guidance for the fastanime project. Does not write implementation code."
|
||||
tools: ['codebase', 'search', 'githubRepo', 'fetch']
|
||||
model: "gpt-4o"
|
||||
---
|
||||
# FastAnime: Feature & Fix Planner Mode
|
||||
|
||||
You are a senior software architect and project planner for the `fastanime` project. You are an expert in its layered architecture (`Core`, `Libs`, `Service`, `CLI`) and its commitment to modular, testable code.
|
||||
|
||||
Your primary goal is to help the user break down a feature request or bug report into a clear, actionable implementation plan.
|
||||
|
||||
**Crucially, you MUST NOT write the full implementation code.** Your output is the plan itself, which will then guide the developer (or another AI agent in "Edit" mode) to write the code.
|
||||
|
||||
### Your Process:
|
||||
|
||||
1. **Understand the Goal:** Start by asking the user to describe the feature they want to build or the bug they want to fix. If they reference a GitHub issue, use the `githubRepo` tool to get the context.
|
||||
|
||||
2. **Analyze the Codebase:** Use the `codebase` and `search` tools to understand how the request fits into the existing architecture. Identify all potentially affected modules, classes, and layers.
|
||||
|
||||
3. **Ask Clarifying Questions:** Ask questions to refine the requirements. For example:
|
||||
* "Will this feature need a new configuration option? If so, what should the default be?"
|
||||
* "How should this behave in the interactive TUI versus the direct CLI command?"
|
||||
* "Which architectural layer does the core logic for this fix belong in?"
|
||||
|
||||
4. **Generate the Implementation Plan:** Once you have enough information, produce a comprehensive plan in the following Markdown format:
|
||||
|
||||
---
|
||||
|
||||
### Implementation Plan: [Feature/Fix Name]
|
||||
|
||||
**1. Overview**
|
||||
> A brief, one-sentence summary of the goal.
|
||||
|
||||
**2. Architectural Impact Analysis**
|
||||
> This is the most important section. Detail which parts of the codebase will be touched and why.
|
||||
> - **Core Layer (`fastanime/core`):**
|
||||
> - *Config (`config/model.py`):* Will a new Pydantic model or field be needed?
|
||||
> - *Utils (`utils/`):* Are any new low-level, reusable functions required?
|
||||
> - *Exceptions (`exceptions.py`):* Does this introduce a new failure case that needs a custom exception?
|
||||
> - **Libs Layer (`fastanime/libs`):**
|
||||
> - *Media API (`media_api/`):* Does this involve a new call to the AniList API?
|
||||
> - *Provider (`provider/`):* Does this affect how data is scraped?
|
||||
> - *Player/Selector (`player/`, `selectors/`):* Does this change how we interact with external tools?
|
||||
> - **Service Layer (`fastanime/cli/service`):**
|
||||
> - Which service will orchestrate this logic? (e.g., `DownloadService`, `PlayerService`). Will a new service be needed?
|
||||
> - **CLI Layer (`fastanime/cli`):**
|
||||
> - *Commands (`commands/`):* Which `click` command(s) will expose this feature?
|
||||
> - *Interactive UI (`interactive/`):* Which TUI menu(s) need to be added or modified?
|
||||
|
||||
**3. Implementation Steps**
|
||||
> A step-by-step checklist for the developer.
|
||||
> 1. [ ] **Config:** Add `new_setting` to `GeneralConfig` in `core/config/model.py`.
|
||||
> 2. [ ] **Core:** Implement `new_util()` in `core/utils/helpers.py`.
|
||||
> 3. [ ] **Service:** Add method `handle_new_feature()` to `MyService`.
|
||||
> 4. [ ] **CLI:** Add `--new-feature` option to the `fastanime anilist search` command.
|
||||
> 5. [ ] **Tests:** Write a unit test for `new_util()` and an integration test for the service method.
|
||||
|
||||
**4. Configuration Changes**
|
||||
> If new settings are needed, list them here and specify which files to update.
|
||||
> - **`core/config/model.py`:** Add field `new_setting: bool`.
|
||||
> - **`core/config/defaults.py`:** Add `GENERAL_NEW_SETTING = False`.
|
||||
> - **`core/config/descriptions.py`:** Add `GENERAL_NEW_SETTING = "Description of the new setting."`
|
||||
|
||||
**5. Testing Strategy**
|
||||
> Briefly describe how to test this change.
|
||||
> - A unit test for the pure logic in the `Core` or `Libs` layer.
|
||||
> - An integration test for the `Service` layer.
|
||||
> - Manual verification steps for the CLI and interactive UI.
|
||||
|
||||
**6. Potential Risks & Open Questions**
|
||||
> - Will this change impact the performance of the provider scraping?
|
||||
> - Do we need to handle a case where the external API does not support this feature?
|
||||
---
|
||||
101
.github/copilot-instructions.md
vendored
Normal file
101
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# GitHub Copilot Instructions for the FastAnime Repository
|
||||
|
||||
Hello, Copilot! This document provides instructions and context to help you understand the `fastanime` codebase. Following these guidelines will help you generate code that is consistent, maintainable, and aligned with the project's architecture.
|
||||
|
||||
## 1. High-Level Project Goal
|
||||
|
||||
`fastanime` is a command-line tool that brings the anime browsing, streaming, and management experience to the terminal. It integrates with metadata providers like AniList and scrapes streaming links from various anime provider websites. The core goals are efficiency, extensibility, and providing a powerful, scriptable user experience.
|
||||
|
||||
## 2. Core Architectural Concepts
|
||||
|
||||
The project follows a clean, layered architecture. When generating code, please adhere to this structure.
|
||||
|
||||
#### Layer 1: CLI (`fastanime/cli`)
|
||||
* **Purpose:** Handles user interaction, command parsing, and displaying output.
|
||||
* **Key Libraries:** `click` for command structure, `rich` for styled output.
|
||||
* **Interactive Mode:** The interactive TUI is managed by the `Session` object in `fastanime/cli/interactive/session.py`. It's a state machine where each menu is a function that returns the next `State` or an `InternalDirective` (like `BACK` or `EXIT`).
|
||||
* **Guideline:** **CLI files should not contain complex business logic.** They should parse arguments and delegate tasks to the Service Layer.
|
||||
|
||||
#### Layer 2: Service (`fastanime/cli/service`)
|
||||
* **Purpose:** Contains the core application logic. Services act as orchestrators, connecting the CLI layer with the various library components.
|
||||
* **Examples:** `DownloadService`, `PlayerService`, `MediaRegistryService`, `WatchHistoryService`.
|
||||
* **Guideline:** When adding new functionality (e.g., a new way to manage downloads), it should likely be implemented in a service or an existing service should be extended. Services are the "brains" of the application.
|
||||
|
||||
#### Layer 3: Libraries (`fastanime/libs`)
|
||||
* **Purpose:** A collection of independent, reusable modules with well-defined contracts (Abstract Base Classes).
|
||||
* **`media_api`:** Interfaces with metadata services like AniList. All new metadata clients **must** inherit from `BaseApiClient`.
|
||||
* **`provider`:** Interfaces with anime streaming websites. All new providers **must** inherit from `BaseAnimeProvider`.
|
||||
* **`player`:** Wrappers around external media players like MPV. All new players **must** inherit from `BasePlayer`.
|
||||
* **`selectors`:** Wrappers for interactive UI tools like FZF or Rofi. All new selectors **must** inherit from `BaseSelector`.
|
||||
* **Guideline:** Libraries should be self-contained and not depend on the CLI or Service layers. They receive configuration and perform their specific task.
|
||||
|
||||
#### Layer 4: Core (`fastanime/core`)
|
||||
* **Purpose:** Foundational code shared across the entire application.
|
||||
* **`config`:** Pydantic models defining the application's configuration structure. **This is the single source of truth for all settings.**
|
||||
* **`downloader`:** The underlying logic for downloading files (using `yt-dlp` or `httpx`).
|
||||
* **`exceptions`:** Custom exception classes used throughout the project.
|
||||
* **`utils`:** Common, low-level utility functions.
|
||||
* **Guideline:** Code in `core` should be generic and have no dependencies on other layers except for other `core` modules.
|
||||
|
||||
## 3. Key Technologies
|
||||
* **Dependency Management:** `uv` is used for all package management and task running. Refer to `pyproject.toml` for dependencies.
|
||||
* **Configuration:** **Pydantic** is used exclusively. The entire configuration is defined in `fastanime/core/config/model.py`.
|
||||
* **CLI Framework:** `click`. We use a custom `LazyGroup` to load commands on demand for faster startup.
|
||||
* **HTTP Client:** `httpx` is the standard for all network requests.
|
||||
|
||||
## 4. How to Add New Features
|
||||
|
||||
Follow these patterns to ensure your contributions fit the existing architecture.
|
||||
|
||||
### How to Add a New Provider
|
||||
1. **Create Directory:** Add a new folder in `fastanime/libs/provider/anime/newprovider/`.
|
||||
2. **Implement `BaseAnimeProvider`:** In `provider.py`, create a class `NewProvider` that inherits from `BaseAnimeProvider` and implement the `search`, `get`, and `episode_streams` methods.
|
||||
3. **Create Mappers:** In `mappers.py`, write functions to convert the provider's API/HTML data into the generic Pydantic models from `fastanime/libs/provider/anime/types.py` (e.g., `SearchResult`, `Anime`, `Server`).
|
||||
4. **Register Provider:**
|
||||
* Add the provider's name to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
|
||||
* Add it to the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
|
||||
|
||||
### How to Add a New Player
|
||||
1. **Create Directory:** Add a new folder in `fastanime/libs/player/newplayer/`.
|
||||
2. **Implement `BasePlayer`:** In `player.py`, create a class `NewPlayer` that inherits from `BasePlayer` and implement the `play` method. It should call the player's executable via `subprocess`.
|
||||
3. **Add Configuration:** If the player has settings, add a `NewPlayerConfig` Pydantic model in `fastanime/core/config/model.py`, and add it to the main `AppConfig`. Also add defaults and descriptions.
|
||||
4. **Register Player:** Add the player's name to the `PLAYERS` list and the factory logic in `fastanime/libs/player/player.py`.
|
||||
|
||||
### How to Add a New Selector
|
||||
1. **Create Directory:** Add a new folder in `fastanime/libs/selectors/newselector/`.
|
||||
2. **Implement `BaseSelector`:** In `selector.py`, create a class `NewSelector` that inherits from `BaseSelector` and implement `choose`, `confirm`, and `ask`.
|
||||
3. **Add Configuration:** If needed, add a `NewSelectorConfig` to `fastanime/core/config/model.py`.
|
||||
4. **Register Selector:** Add the selector's name to the `SELECTORS` list and the factory logic in `fastanime/libs/selectors/selector.py`. Update the `Literal` type hint for `selector` in `GeneralConfig`.
|
||||
|
||||
### How to Add a New CLI Command
|
||||
* **Top-Level Command (`fastanime my-command`):**
|
||||
1. Create `fastanime/cli/commands/my_command.py` with your `click.command()`.
|
||||
2. Register it in the `commands` dictionary in `fastanime/cli/cli.py`.
|
||||
* **Subcommand (`fastanime anilist my-subcommand`):**
|
||||
1. Create `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
|
||||
2. Register it in the `lazy_subcommands` dictionary of the parent `click.group()` (e.g., in `fastanime/cli/commands/anilist/cmd.py`).
|
||||
|
||||
### How to Add a New Configuration Option
|
||||
1. **Add to Model:** Add the field to the appropriate Pydantic model in `fastanime/core/config/model.py`.
|
||||
2. **Add Default:** Add a default value in `fastanime/core/config/defaults.py`.
|
||||
3. **Add Description:** Add a user-friendly description in `fastanime/core/config/descriptions.py`.
|
||||
4. The config loader and CLI option generation will handle the rest automatically.
|
||||
|
||||
## 5. Code Style and Conventions
|
||||
* **Style:** `ruff` for formatting, `ruff` for linting. The `pre-commit` hooks handle this.
|
||||
* **Types:** Full type hinting is mandatory. All code must pass `pyright`.
|
||||
* **Commits:** Adhere to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
|
||||
* **Logging:** Use Python's `logging` module. Do not use `print()` for debugging or informational messages in library or service code.
|
||||
|
||||
## 6. Do's and Don'ts
|
||||
|
||||
* ✅ **DO** use the abstract base classes (`BaseProvider`, `BasePlayer`, etc.) as contracts.
|
||||
* ✅ **DO** place business logic in the `service` layer.
|
||||
* ✅ **DO** use the Pydantic models in `fastanime/core/config/model.py` as the single source of truth for configuration.
|
||||
* ✅ **DO** use the `Context` object in interactive menus to access services and configuration.
|
||||
|
||||
* ❌ **DON'T** hardcode configuration values. Access them via the `config` object.
|
||||
* ❌ **DON'T** put complex logic directly into `click` command functions. Delegate to a service.
|
||||
* ❌ **DON'T** make direct `httpx` calls outside of a `provider` or `media_api` library.
|
||||
* ❌ **DON'T** introduce new dependencies without updating `pyproject.toml` and discussing it first.
|
||||
|
||||
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@@ -1,38 +1,43 @@
|
||||
name: debug_build
|
||||
name: build
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Test Workflow"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
debug_build:
|
||||
build:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install
|
||||
- name: build app
|
||||
run: poetry build
|
||||
enable-cache: true
|
||||
|
||||
- name: Build fastanime
|
||||
run: uv build
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fastanime_debug_build
|
||||
path: |
|
||||
dist
|
||||
!dist/*.whl
|
||||
# - name: Run the automated tests (for example)
|
||||
# run: poetry run pytest -v
|
||||
|
||||
- name: Install nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
|
||||
- name: Use GitHub Action built-in cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
|
||||
- name: Nix Flake check (evaluation + tests)
|
||||
run: nix flake check
|
||||
|
||||
- name: Build the nix derivation
|
||||
run: nix build
|
||||
|
||||
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@@ -27,11 +27,13 @@ jobs:
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
# NOTE: put your own distribution build steps here.
|
||||
python -m pip install build
|
||||
python -m build
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
enable-cache: true
|
||||
|
||||
- name: Build fastanime
|
||||
run: uv build
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
40
.github/workflows/test.yml
vendored
40
.github/workflows/test.yml
vendored
@@ -6,37 +6,35 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11"] # List the Python versions you want to test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
uses: abatilo/actions-poetry@v2
|
||||
- name: Setup a local virtual environment (if no poetry.toml file)
|
||||
run: |
|
||||
poetry config virtualenvs.create true --local
|
||||
poetry config virtualenvs.in-project true --local
|
||||
- uses: actions/cache@v3
|
||||
name: Define a cache for the virtual environment based on the dependencies lock file
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
path: ./.venv
|
||||
key: venv-${{ hashFiles('poetry.lock') }}
|
||||
- name: Install the project dependencies
|
||||
run: poetry install
|
||||
- name: run linter, formatters and sort imports
|
||||
run: |
|
||||
poetry run black .
|
||||
poetry run ruff check --output-format=github . --fix
|
||||
poetry run isort . --profile black
|
||||
- name: run type checking
|
||||
run: poetry run pyright
|
||||
- name: run tests
|
||||
run: poetry run pytest
|
||||
enable-cache: true
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Run linter and formater
|
||||
run: uv run ruff check --output-format=github
|
||||
|
||||
- name: Run type checking
|
||||
run: uv run pyright
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest tests
|
||||
|
||||
87
.gitignore
vendored
87
.gitignore
vendored
@@ -1,25 +1,16 @@
|
||||
# mine
|
||||
*.mp4
|
||||
*.mp3
|
||||
*.ass
|
||||
vids
|
||||
data/
|
||||
.project/
|
||||
fastanime.ini
|
||||
crashdump.txt
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
anixstream.ini
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
bin/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -39,7 +30,7 @@ MANIFEST
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
# *.spec
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
@@ -55,7 +46,7 @@ htmlcov/
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
@@ -103,23 +94,36 @@ ipython_config.py
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
@@ -168,11 +172,42 @@ cython_debug/
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
app/anixstream.ini
|
||||
app/settings.json
|
||||
app/user_data.json
|
||||
app/View/SearchScreen/.search_screen.py.un~
|
||||
app/View/SearchScreen/search_screen.py~
|
||||
app/user_data.json
|
||||
.buildozer
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# custom
|
||||
repomix-output.xml
|
||||
.project/
|
||||
result
|
||||
.direnv
|
||||
fastanime/libs/provider/anime/hianime/extractors/js/node_modules
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
default_language_version:
|
||||
python: python3.10
|
||||
python: python3.12
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
@@ -7,7 +7,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: ["--profile", "black"] # Ensure compatibility with Black
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.2.1
|
||||
@@ -19,17 +19,15 @@ repos:
|
||||
"--remove-unused-variables",
|
||||
"--remove-all-unused-imports",
|
||||
]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.4.10
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [--fix]
|
||||
# - repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# rev: v0.4.10
|
||||
# hooks:
|
||||
# - id: ruff
|
||||
# args: [--fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
name: black
|
||||
language_version: python3.10 # to ensure compatibilty
|
||||
#language_version: python3.10
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
208
CONTRIBUTIONS.md
Normal file
208
CONTRIBUTIONS.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Contributing to FastAnime
|
||||
|
||||
First off, thank you for considering contributing to FastAnime! We welcome any help, whether it's reporting a bug, proposing a feature, or writing code. This document will guide you through the process.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
There are many ways to contribute to the FastAnime project:
|
||||
|
||||
* **Reporting Bugs:** If you find a bug, please create an issue in our [issue tracker](https://github.com/Benexl/FastAnime/issues).
|
||||
* **Suggesting Enhancements:** Have an idea for a new feature or an improvement to an existing one? We'd love to hear it.
|
||||
* **Writing Code:** Help us fix bugs or implement new features.
|
||||
* **Improving Documentation:** Enhance our README, add examples, or clarify our contribution guidelines.
|
||||
* **Adding a Provider, Player, or Selector:** Extend FastAnime's capabilities by integrating new tools and services.
|
||||
|
||||
## Contribution Workflow
|
||||
|
||||
We follow the standard GitHub Fork & Pull Request workflow.
|
||||
|
||||
1. **Create an Issue:** Before starting work on a new feature or a significant bug fix, please [create an issue](https://github.com/Benexl/FastAnime/issues/new/choose) to discuss your idea. This allows us to give feedback and prevent duplicate work. For small bugs or documentation typos, you can skip this step.
|
||||
|
||||
2. **Fork the Repository:** Create your own fork of the FastAnime repository.
|
||||
|
||||
3. **Clone Your Fork:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/FastAnime.git
|
||||
cd FastAnime
|
||||
```
|
||||
|
||||
4. **Create a Branch:** Create a new branch for your changes. Use a descriptive name.
|
||||
```bash
|
||||
# For a new feature
|
||||
git checkout -b feat/my-new-feature
|
||||
|
||||
# For a bug fix
|
||||
git checkout -b fix/bug-description
|
||||
```
|
||||
|
||||
5. **Make Your Changes:** Write your code, following the guidelines below.
|
||||
|
||||
6. **Run Quality Checks:** Before committing, ensure your code passes all quality checks.
|
||||
```bash
|
||||
# Format, lint, and sort imports
|
||||
uv run ruff check --fix .
|
||||
uv run ruff format .
|
||||
|
||||
# Run type checking
|
||||
uv run pyright
|
||||
|
||||
# Run tests
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
7. **Commit Your Changes:** We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. This helps us automate releases and makes the commit history more readable.
|
||||
```bash
|
||||
# Example commit messages
|
||||
git commit -m "feat: add support for XYZ provider"
|
||||
git commit -m "fix(anilist): correctly parse episode numbers with decimals"
|
||||
git commit -m "docs: update installation instructions in README"
|
||||
git commit -m "chore: upgrade httpx to version 0.28.1"
|
||||
```
|
||||
|
||||
8. **Push to Your Fork:**
|
||||
```bash
|
||||
git push origin feat/my-new-feature
|
||||
```
|
||||
|
||||
9. **Submit a Pull Request:** Open a pull request from your branch to the `master` branch of the main FastAnime repository. Provide a clear title and description of your changes.
|
||||
|
||||
## Setting Up Your Development Environment
|
||||
|
||||
### Prerequisites
|
||||
* Git
|
||||
* Python 3.10+
|
||||
* [uv](https://github.com/astral-sh/uv) (recommended)
|
||||
* **External Tools (for full functionality):** `mpv`, `fzf`, `rofi`, `webtorrent-cli`, `ffmpeg`.
|
||||
|
||||
### Nix / NixOS Users
|
||||
The easiest way to get a development environment with all dependencies is to use our Nix flake.
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
This command will drop you into a shell with all the necessary tools and a Python environment ready to go.
|
||||
|
||||
### Standard Setup (uv + venv)
|
||||
|
||||
1. **Clone your fork** (as described above).
|
||||
|
||||
2. **Create and activate a virtual environment:**
|
||||
```bash
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
3. **Install all dependencies:** This command installs both runtime and development dependencies, including all optional extras.
|
||||
```bash
|
||||
uv sync --all-extras --dev
|
||||
```
|
||||
|
||||
4. **Set up pre-commit hooks:** This will automatically run linters and formatters before each commit, ensuring your code meets our quality standards.
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
To maintain code quality and consistency, please adhere to the following guidelines.
|
||||
|
||||
* **Formatting:** We use **Black** for code formatting and **isort** (via Ruff) for import sorting. The pre-commit hooks will handle this for you.
|
||||
* **Linting:** We use **Ruff** for linting. Please ensure your code has no linting errors before submitting a PR.
|
||||
* **Type Hinting:** All new code should be fully type-hinted and pass `pyright` checks. We rely on Pydantic for data validation and configuration, so leverage it where possible.
|
||||
* **Modularity and Architecture:**
|
||||
* **Services:** Business logic is organized into services (e.g., `PlayerService`, `DownloadService`).
|
||||
* **Factories:** Use factory patterns (`create_provider`, `create_selector`) for creating instances of different implementations.
|
||||
* **Configuration:** All configuration is managed through Pydantic models in `fastanime/core/config/model.py`. When adding new config options, update the model, defaults, and descriptions.
|
||||
* **Commit Messages:** Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard.
|
||||
* **Testing:** New features should be accompanied by tests. Bug fixes should ideally include a regression test.
|
||||
|
||||
## How to Add a New Provider
|
||||
|
||||
Adding a new anime provider is a great way to contribute. Here are the steps:
|
||||
|
||||
1. **Create a New Provider Directory:** Inside `fastanime/libs/provider/anime/`, create a new directory with the provider's name (e.g., `fastanime/libs/provider/anime/newprovider/`).
|
||||
|
||||
2. **Implement the Provider:**
|
||||
* Create a `provider.py` file.
|
||||
* Define a class (e.g., `NewProviderApi`) that inherits from `BaseAnimeProvider`.
|
||||
* Implement the abstract methods: `search`, `get`, and `episode_streams`.
|
||||
* Create `mappers.py` to convert the provider's data structures into the generic types defined in `fastanime/libs/provider/anime/types.py`.
|
||||
* Create `types.py` for any provider-specific data structures you need.
|
||||
* If the provider requires complex scraping, place extractor logic in an `extractors/` subdirectory.
|
||||
|
||||
3. **Register the Provider:**
|
||||
* Add your new provider to the `ProviderName` enum in `fastanime/libs/provider/anime/types.py`.
|
||||
* Register it in the `PROVIDERS_AVAILABLE` dictionary in `fastanime/libs/provider/anime/provider.py`.
|
||||
|
||||
4. **Add Normalization Rules (Optional):** If the provider uses different anime titles than AniList, add mappings to `fastanime/assets/normalizer.json`.
|
||||
|
||||
## How to Add a New Player
|
||||
|
||||
1. **Create a New Player Directory:** Inside `fastanime/libs/player/`, create a directory for your player (e.g., `fastanime/libs/player/myplayer/`).
|
||||
|
||||
2. **Implement the Player Class:**
|
||||
* In `myplayer/player.py`, create a class (e.g., `MyPlayer`) that inherits from `BasePlayer`.
|
||||
* Implement the required abstract methods: `play(self, params: PlayerParams)` and `play_with_ipc(self, params: PlayerParams, socket_path: str)`. The IPC method is optional but recommended for advanced features.
|
||||
* The `play` method should handle launching the player as a subprocess and return a `PlayerResult`.
|
||||
|
||||
3. **Add Configuration (if needed):**
|
||||
* If your player has configurable options, add a new Pydantic model (e.g., `MyPlayerConfig`) in `fastanime/core/config/model.py`. It should inherit from `OtherConfig`.
|
||||
* Add this new config model as a field in the main `AppConfig` model.
|
||||
* Add default values in `defaults.py` and descriptions in `descriptions.py`.
|
||||
|
||||
4. **Register the Player:**
|
||||
* Add your player's name to the `PLAYERS` list in `fastanime/libs/player/player.py`.
|
||||
* Add the logic to instantiate your player class within the `PlayerFactory.create` method.
|
||||
|
||||
## How to Add a New Selector
|
||||
|
||||
1. **Create a New Selector Directory:** Inside `fastanime/libs/selectors/`, create a new directory (e.g., `fastanime/libs/selectors/myselector/`).
|
||||
|
||||
2. **Implement the Selector Class:**
|
||||
* In `myselector/selector.py`, create a class (e.g., `MySelector`) that inherits from `BaseSelector`.
|
||||
* Implement the abstract methods: `choose`, `confirm`, and `ask`.
|
||||
* Optionally, you can override `choose_multiple` and `search` for more advanced functionality.
|
||||
|
||||
3. **Add Configuration (if needed):** Follow the same configuration steps as for adding a new player.
|
||||
|
||||
4. **Register the Selector:**
|
||||
* Add your selector's name to the `SELECTORS` list in `fastanime/libs/selectors/selector.py`.
|
||||
* Add the instantiation logic to the `SelectorFactory.create` method.
|
||||
* Update the `Literal` type hint for the `selector` field in `GeneralConfig` (`fastanime/core/config/model.py`).
|
||||
|
||||
## How to Add a New CLI Command or Service
|
||||
|
||||
Our CLI uses `click` and a `LazyGroup` class to load commands on demand.
|
||||
|
||||
### Adding a Top-Level Command (e.g., `fastanime my-command`)
|
||||
|
||||
1. **Create the Command File:** Create a new Python file in `fastanime/cli/commands/` (e.g., `my_command.py`). This file should contain your `click.command()` function.
|
||||
|
||||
2. **Register the Command:** In `fastanime/cli/cli.py`, add your command to the `commands` dictionary.
|
||||
```python
|
||||
commands = {
|
||||
# ... existing commands
|
||||
"my-command": "my_command.my_command_function",
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a Subcommand (e.g., `fastanime anilist my-subcommand`)
|
||||
|
||||
1. **Create the Command File:** Place your new command file inside the appropriate subdirectory, for example, `fastanime/cli/commands/anilist/commands/my_subcommand.py`.
|
||||
|
||||
2. **Register the Subcommand:** In the parent command's entry point file (e.g., `fastanime/cli/commands/anilist/cmd.py`), add your subcommand to the `commands` dictionary within the `LazyGroup`.
|
||||
```python
|
||||
@click.group(
|
||||
cls=LazyGroup,
|
||||
# ... other options
|
||||
lazy_subcommands={
|
||||
# ... existing subcommands
|
||||
"my-subcommand": "my_subcommand.my_subcommand_function",
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Creating a Service
|
||||
If your command involves complex logic, consider creating a service in `fastanime/cli/service/` to keep the business logic separate from the command-line interface. This service can then be instantiated and used within your `click` command function. This follows the existing pattern for services like `DownloadService` and `PlayerService`.
|
||||
|
||||
---
|
||||
Thank you for contributing to FastAnime
|
||||
38
DISCLAIMER.md
Normal file
38
DISCLAIMER.md
Normal file
@@ -0,0 +1,38 @@
|
||||
<h1 align="center">Disclaimer</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
<h2>This project: fastanime</h2>
|
||||
|
||||
<br>
|
||||
|
||||
The core aim of this project is to co-relate automation and efficiency to extract what is provided to a user on the internet. All content available through the project is hosted by external non-affiliated sources.
|
||||
|
||||
<br>
|
||||
|
||||
<b>All content served through this project is publicly accessible. If your site is listed in this project, the code is pretty much public. Take necessary measures to counter the exploits used to extract content in your site.</b>
|
||||
|
||||
Think of this project as your normal browser, but a bit more straight-forward and specific. While an average browser makes hundreds of requests to get everything from a site, this project goes on to only make requests associated with getting the content served by the sites.
|
||||
|
||||
<b>
|
||||
|
||||
This project is to be used at the user's own risk, based on their government and laws.
|
||||
|
||||
This project has no control on the content it is serving, using copyrighted content from the providers is not going to be accounted for by the developer. It is the user's own risk.
|
||||
|
||||
</b>
|
||||
|
||||
<br>
|
||||
|
||||
<h2>DMCA and Copyright Infrigements</h3>
|
||||
|
||||
<br>
|
||||
|
||||
<b>
|
||||
|
||||
A browser is a tool, and the maliciousness of the tool is directly based on the user.
|
||||
</b>
|
||||
|
||||
This project uses client-side content access mechanisms. Hence, the copyright infrigements or DMCA in this project's regards are to be forwarded to the associated site by the associated notifier of any such claims. This is one of the main reasons the sites are listed in this project.
|
||||
|
||||
<b>Do not harass the developer. Any personal information about the developer is intentionally not made public. Exploiting such information without consent in regards to this topic will lead to legal actions by the developer themselves.</b>
|
||||
10
Dockerfile
10
Dockerfile
@@ -1,10 +0,0 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install python3
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install pipx
|
||||
RUN pipx ensurepath
|
||||
COPY . /fastanime
|
||||
WORKDIR /fastanime
|
||||
RUN pipx install .
|
||||
CMD ["bash"]
|
||||
949
README.md
949
README.md
@@ -1,602 +1,361 @@
|
||||
# FastAnime
|
||||
|
||||
Welcome to **FastAnime**, anime site experience from the terminal.
|
||||
|
||||
**fzf mode**
|
||||
|
||||
[fa_fzf_demo.webm](https://github.com/user-attachments/assets/b1fecf25-e358-4e8b-a144-bcb7947210cf)
|
||||
|
||||
**other modes:**
|
||||
|
||||
<details>
|
||||
<summary><b>rofi mode</b></summary>
|
||||
|
||||
[fa_rofi_mode.webm](https://github.com/user-attachments/assets/2ce669bf-b62f-4c44-bd79-cf0dcaddf37a)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Default mode</b></summary>
|
||||
|
||||
[fa_default_mode.webm](https://github.com/user-attachments/assets/1ce3a23d-f4a0-4bc1-8518-426ec7b3b69e)
|
||||
|
||||
</details>
|
||||
|
||||
Heavily inspired by [animdl](https://github.com/justfoolingaround/animdl), [magic-tape](https://gitlab.com/christosangel/magic-tape/-/tree/main?ref_type=heads) and [ani-cli](https://github.com/pystardust/ani-cli).
|
||||
|
||||
<!--toc:start-->
|
||||
|
||||
- [FastAnime](#fastanime)
|
||||
- [Installation](#installation)
|
||||
- [Installation using your favourite package manager](#installation-using-your-favourite-package-manager)
|
||||
- [Using pipx](#using-pipx)
|
||||
- [Using pip](#using-pip)
|
||||
- [Installing the bleeding edge version](#installing-the-bleeding-edge-version)
|
||||
- [Building from the source](#building-from-the-source)
|
||||
- [External Dependencies](#external-dependencies)
|
||||
- [Usage](#usage)
|
||||
- [The Commandline interface :fire:](#the-commandline-interface-fire)
|
||||
- [The anilist command :fire: :fire: :fire:](#the-anilist-command-fire-fire-fire)
|
||||
- [Running without any subcommand](#running-without-any-subcommand)
|
||||
- [Subcommands](#subcommands)
|
||||
- [download subcommand](#download-subcommand)
|
||||
- [search subcommand](#search-subcommand)
|
||||
- [downloads subcommand](#downloads-subcommand)
|
||||
- [config subcommand](#config-subcommand)
|
||||
- [cache subcommand](#cache-subcommand)
|
||||
- [update subcommand](#update-subcommand)
|
||||
- [completions subcommand](#completions-subcommand)
|
||||
- [MPV specific commands](#mpv-specific-commands)
|
||||
- [Added keybindings](#added-keybindings)
|
||||
- [Added script messages](#added-script-messages)
|
||||
- [Configuration](#configuration)
|
||||
- [Contributing](#contributing)
|
||||
- [Receiving Support](#receiving-support)
|
||||
- [Supporting the Project](#supporting-the-project)
|
||||
<!--toc:end-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project currently scrapes allanime and animepahe and is in no way related to them nor does the project own any content servers. The site is in the public domain and can be access by any one with a browser.
|
||||
|
||||
## Installation
|
||||
|
||||
The app can run wherever python can run. So all you need to have is python installed on your device.
|
||||
On android you can use [termux](https://github.com/termux/termux-app).
|
||||
If you have any difficulty consult for help on the [discord channel](https://discord.gg/HRjySFjQ)
|
||||
|
||||
### Installation using your favourite package manager
|
||||
|
||||
Currently the app is only published on [pypi](https://pypi.org/project/fastanime/).
|
||||
|
||||
#### Using pipx
|
||||
|
||||
Preferred method of installation since [Pipx](https://github.com/pypa/pipx) creates an isolated environment for each app it installs.
|
||||
|
||||
```bash
|
||||
|
||||
pipx install fastanime
|
||||
# -- or for the development version --
|
||||
pipx install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pipx install 'fastanime==0.60.1.dev1'
|
||||
|
||||
```
|
||||
|
||||
#### Using pip
|
||||
|
||||
```bash
|
||||
pip install fastanime
|
||||
# -- or for the development version --
|
||||
pip install 'fastanime==<latest-pre-release-tag>.dev1'
|
||||
# example
|
||||
# pip install 'fastanime==0.60.1.dev1'
|
||||
```
|
||||
|
||||
### Installing the bleeding edge version
|
||||
|
||||
To install the latest build which are created on every push by GitHub actions, download the [fastanime_debug_build](https://github.com/Benex254/FastAnime/actions) of your choosing from the GitHub actions page.
|
||||
Then:
|
||||
|
||||
```bash
|
||||
unzip fastanime_debug_build
|
||||
|
||||
# outputs fastanime<version>.tar.gz
|
||||
|
||||
pipx install fastanime<version>.tar.gz
|
||||
|
||||
# --- or ---
|
||||
|
||||
pip install fastanime<version>.tar.gz
|
||||
```
|
||||
|
||||
### Building from the source
|
||||
|
||||
Requirements:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [python 3.10 and above](https://www.python.org/)
|
||||
- [poetry](https://python-poetry.org/docs/#installation)
|
||||
|
||||
To build from the source, follow these steps:
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/Benex254/FastAnime.git`
|
||||
2. Navigate into the folder: `cd FastAnime`
|
||||
3. Then build and Install the app:
|
||||
|
||||
```bash
|
||||
# Normal Installation
|
||||
poetry build
|
||||
cd dist
|
||||
pip install fastanime<version>.whl
|
||||
|
||||
# Editable installation (easiest for updates)
|
||||
# just do a git pull in the Project dir
|
||||
# the latter will require rebuilding the app
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
4. Enjoy! Verify installation with:
|
||||
|
||||
```bash
|
||||
fastanime --version
|
||||
```
|
||||
|
||||
> [!Tip]
|
||||
>
|
||||
> Download the completions from [here](https://github.com/Benex254/FastAnime/tree/master/completions) for your shell.
|
||||
> To add completions:
|
||||
>
|
||||
> - Fish Users: `cp $FASTANIME_PATH/completions/fastanime.fish ~/.config/fish/completions/`
|
||||
> - Bash Users: Add `source $FASTANIME_PATH/completions/fastanime.bash` to your `.bashrc`
|
||||
> - Zsh Users: Add `source $FASTANIME_PATH/completions/fastanime.zsh` to your `.zshrc`
|
||||
|
||||
### External Dependencies
|
||||
|
||||
The only required external dependency, unless you won't be streaming, is [MPV](https://mpv.io/installation/), which i recommend installing with [uosc](https://github.com/tomasklaen/uosc) :fire: and [thumbfast](https://github.com/po5/thumbfast) for the best experience since they add a better interface to it.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The project currently sees no reason to support any other video
|
||||
> player because we believe nothing beats **MPV** and it provides
|
||||
> everything you could ever need with a small footprint.
|
||||
> But if you have a reason feel free to encourage as to do so.
|
||||
|
||||
**Other external dependencies that will just make your experience better:**
|
||||
|
||||
- [fzf](https://github.com/junegunn/fzf) 🔥 which is used as a better alternative to the ui.
|
||||
- [rofi](https://github.com/davatorium/rofi) 🔥 which is used as another alternative ui + the the desktop entry ui
|
||||
- [chafa](https://github.com/hpjansson/chafa) currently the best cross platform and cross terminal image viewer for the terminal.
|
||||
- [icat](https://sw.kovidgoyal.net/kitty/kittens/icat/) an image viewer that only works in [kitty terminal](https://sw.kovidgoyal.net/kitty/), which is currently the best terminal in my opinion, and by far the best image renderer for the terminal thanks to kitty's terminal graphics protocol. Its terminal graphics is so op that you can [run a browser on it](https://github.com/chase/awrit?tab=readme-ov-file)!!
|
||||
- [bash](https://www.gnu.org/software/bash/) is used as the preview script language.
|
||||
- [ani-skip](https://github.com/synacktraa/ani-skip) used for skipping the opening and ending theme songs
|
||||
|
||||
## Usage
|
||||
|
||||
The app offers both a graphical interface (under development) and a robust command-line interface.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The GUI is mostly in hiatus; use the CLI for now.
|
||||
> However, you can try it out before i decided to change my objective by checking out this [release](https://github.com/Benex254/FastAnime/tree/v0.20.0).
|
||||
> But be reassured for those who aren't terminal chads, i will still complete the GUI for the fun of it
|
||||
|
||||
### The Commandline interface :fire:
|
||||
|
||||
Designed for power users who prefer efficiency over browser-based streaming and still want the experience in their terminal.
|
||||
|
||||
Overview of main commands:
|
||||
|
||||
- `fastanime anilist`: Powerful command for browsing and exploring anime due to AniList integration.
|
||||
- `fastanime download`: Download anime.
|
||||
- `fastanime search`: Powerful command meant for binging since it doesn't require the interfaces
|
||||
- `fastanime downloads`: View downloaded anime and watch with MPV.
|
||||
- `fastanime config`: Quickly edit configuration settings.
|
||||
- `fastanime cache`: Quickly manage the cache fastanime uses
|
||||
|
||||
Configuration is directly passed into this command at run time to override your config.
|
||||
|
||||
Available options for the fastanime command include:
|
||||
|
||||
- `--server <server>` or `-s <server>` set the default server to auto select
|
||||
- `--continue/--no-continue` or `-c/-no-c` whether to continue from the last episode you were watching
|
||||
- `--local-history/--remote-history` whether to use remote or local history defaults to local
|
||||
- `--quality <1080/720/480/360>` or `-q <1080/720/480/360>` the link to choose from server
|
||||
- `--translation-type <dub/sub>` or `-t <dub/sub>` what language for anime
|
||||
- `--dub` dubbed anime
|
||||
- `--sub` subbed anime
|
||||
- `--auto-select/--no-auto-select` or `-a/-no-a` auto select title from provider results
|
||||
- `--auto-next/--no-auto-next` or `-A/-no-A` auto select next episode
|
||||
- `-downloads-dir <path>` or `-d <path>` set the folder to download anime into
|
||||
- `--fzf` use fzf for the ui
|
||||
- `--default` use the default ui
|
||||
- `--preview` show a preview when using fzf
|
||||
- `--no-preview` dont show a preview when using fzf
|
||||
- `--format <yt-dlp format string>` or `-f <yt-dlp format string>` set the format of anime downloaded and streamed based on yt-dlp format. Works when `--server gogoanime`
|
||||
- `--icons/--no-icons` toggle the visibility of the icons
|
||||
- `--skip/--no-skip` whether to skip the opening and ending theme songs.
|
||||
- `--rofi` use rofi for the ui
|
||||
- `--rofi-theme <path>` theme to use with rofi
|
||||
- `--rofi-theme-input <path>` theme to use with rofi input
|
||||
- `--rofi-theme-confirm <path>` theme to use with rofi confirm
|
||||
- `--log` allow logging to stdout
|
||||
- `--log-file` allow logging to a file
|
||||
- `--rich-traceback` allow rich traceback
|
||||
- `--use-mpv-mod/--use-default-player` whether to use python-mpv
|
||||
- `--provider <allanime>` anime site of choice to scrape from
|
||||
- `--sync-play` or `-sp` use syncplay for streaming anime so you can watch with your friends
|
||||
|
||||
Example usage of the above options
|
||||
|
||||
```bash
|
||||
# example of syncplay intergration
|
||||
fastanime --sync-play search -t <anime-title>
|
||||
|
||||
# --- or ---
|
||||
|
||||
# to watch with anilist intergration
|
||||
fastanime --sync-play anilist
|
||||
|
||||
# downloading dubbed anime
|
||||
fastanime --dub download <anime>
|
||||
|
||||
# use icons and fzf for a more elegant ui with preview
|
||||
fastanime --icons --preview --fzf anilist
|
||||
|
||||
# use icons with default ui
|
||||
fastanime --icons --default anilist
|
||||
```
|
||||
|
||||
#### The anilist command :fire: :fire: :fire:
|
||||
|
||||
Stream, browse, and discover anime efficiently from the terminal using the [AniList API](https://github.com/AniList/ApiV2-GraphQL-Docs).
|
||||
|
||||
##### Running without any subcommand
|
||||
|
||||
Run `fastanime anilist` to access the main interface.
|
||||
|
||||
##### Subcommands
|
||||
|
||||
The subcommands are mainly their as convenience. Since all the features already exist in the main interface.
|
||||
|
||||
- `fastanime anilist trending`: Top 15 trending anime.
|
||||
- `fastanime anilist recent`: Top 15 recently updated anime.
|
||||
- `fastanime anilist search`: Search for anime (top 50 results).
|
||||
- `fastanime anilist upcoming`: Top 15 upcoming anime.
|
||||
- `fastanime anilist popular`: Top 15 popular anime.
|
||||
- `fastanime anilist favourites`: Top 15 favorite anime.
|
||||
- `fastanime anilist random`: get random anime
|
||||
|
||||
The following are commands you can only run if you are signed in to your AniList account:
|
||||
|
||||
- `fastanime anilist watching`
|
||||
- `fastanime anilist planning`
|
||||
- `fastanime anilist rewatching`
|
||||
- `fastanime anilist dropped`
|
||||
- `fastanime anilist paused`
|
||||
- `fastanime anilist completed`
|
||||
|
||||
Plus: `fastanime anilist notifier` :fire:
|
||||
|
||||
```bash
|
||||
# basic form
|
||||
fastanime anilist notifier
|
||||
|
||||
# with logging to stdout
|
||||
fastanime --log anilist notifier
|
||||
|
||||
# with logging to a file. stored in the same place as your config
|
||||
fastanime --log-file anilist notifier
|
||||
```
|
||||
|
||||
The above commands will start a loop that checks every 2 minutes if any of the anime in your watch list that are aireing has just released a new episode.
|
||||
|
||||
The notification will consist of a cover image of the anime in none windows systems.
|
||||
|
||||
You can place the command among your machines startup scripts.
|
||||
|
||||
For fish users for example you can decide to put this in your `~/.config/fish/config.fish`:
|
||||
|
||||
```fish
|
||||
if ! ps aux | grep -q '[f]astanime .* notifier'
|
||||
echo initializing fastanime anilist notifier
|
||||
nohup fastanime --log-file anilist notifier>/dev/null &
|
||||
end
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> To sign in just run `fastanime anilist login` and follow the instructions.
|
||||
> To view your login status `fastanime anilist login --status`
|
||||
> To erase login data `fastanime anilist login --erase`
|
||||
|
||||
#### download subcommand
|
||||
|
||||
Download anime to watch later dub or sub with this one command.
|
||||
Its optimized for scripting due to fuzzy matching; basically you don't have to manually select search results.
|
||||
So every step of the way has been and can be automated.
|
||||
Uses a list slicing syntax similar to that of python as the value for the `-r` option.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The download feature is powered by [yt-dlp](https://github.com/yt-dlp/yt-dlp) so all the bells and whistles that it provides are readily available in the project.
|
||||
> Like continuing from where you left of while downloading, after lets say you lost your internet connection.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# Download all available episodes
|
||||
# multiple titles can be specified with -t option
|
||||
fastanime download -t <anime-title> -t <anime-title>
|
||||
# -- or --
|
||||
fastanime download -t <anime-title> -t <anime-title> -r ':'
|
||||
|
||||
# download latest episode for the two anime titles
|
||||
# the number can be any no of latest episodes but a minus sign
|
||||
# must be present
|
||||
fastanime download -t <anime-title> -t <anime-title> -r '-1'
|
||||
|
||||
# latest 5
|
||||
fastanime download -t <anime-title> -t <anime-title> -r '-5'
|
||||
|
||||
# Download specific episode range
|
||||
# be sure to observe the range Syntax
|
||||
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>:<step>'
|
||||
|
||||
fastanime download <anime-title> -r '<episodes-start>:<episodes-end>'
|
||||
|
||||
fastanime download <anime-title> -r '<episodes-start>:'
|
||||
|
||||
fastanime download <anime-title> -r ':<episodes-end>'
|
||||
```
|
||||
|
||||
#### search subcommand
|
||||
|
||||
Powerful command mainly aimed at binging anime. Since it doesn't require interaction with the interfaces.
|
||||
Uses a list slicing syntax similar to that of python as the value of the `-r` option.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# basic form where you will still be prompted for the episode number
|
||||
# multiple titles can be specified with the -t option
|
||||
fastanime search -t <anime-title> -t <anime-title>
|
||||
|
||||
# binge all episodes with this command
|
||||
fastanime search -t <anime-title> -r ':'
|
||||
|
||||
# watch latest episode
|
||||
fastanime search -t <anime-title> -r '-1'
|
||||
|
||||
# binge a specific episode range with this command
|
||||
# be sure to observe the range Syntax
|
||||
fastanime search -t <anime-title> -r '<start>:<stop>'
|
||||
|
||||
fastanime search -t <anime-title> -r '<start>:<stop>:<step>'
|
||||
|
||||
fastanime search -t <anime-title> -r '<start>:'
|
||||
|
||||
fastanime search -t <anime-title> -r ':<end>'
|
||||
```
|
||||
|
||||
#### downloads subcommand
|
||||
|
||||
View and stream the anime you downloaded using MPV.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
fastanime downloads
|
||||
|
||||
# view individual episodes
|
||||
fastanime downloads --view-episodes
|
||||
# --- or ---
|
||||
fastanime downloads -v
|
||||
|
||||
# to set seek time when using ffmpegthumbnailer for local previews
|
||||
# -1 means random and is the default
|
||||
fastanime downloads --time-to-seek <intRange(-1,100)>
|
||||
# --- or ---
|
||||
fastanime downloads -t <intRange(-1,100)>
|
||||
|
||||
# to get the path to the downloads folder set
|
||||
fastanime downloads --path
|
||||
# useful when you want to use the value for other programs
|
||||
|
||||
```
|
||||
|
||||
#### config subcommand
|
||||
|
||||
Edit FastAnime configuration settings using your preferred editor (based on `$EDITOR` environment variable so be sure to set it).
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
fastanime config
|
||||
|
||||
# to get config path which is useful if you want to use it for another program.
|
||||
fastanime config --path
|
||||
|
||||
# add a desktop entry
|
||||
fastanime config --desktop-entry
|
||||
|
||||
# view current contents of your configuration or can be used to get an example config
|
||||
fastanime config --view
|
||||
```
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> If it opens [vim](https://www.vim.org/download.php) you can exit by typing `:q` .
|
||||
|
||||
#### cache subcommand
|
||||
|
||||
Easily manage the data fastanime has cached; for the previews.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# delete everything in the cache dir
|
||||
fastanime cache --clean
|
||||
|
||||
# print the path to the cache dir and exit
|
||||
fastanime cache --path
|
||||
|
||||
# print the current size of the cache dir and exit
|
||||
fastanime cache --size
|
||||
|
||||
# open the cache dir and exit
|
||||
fastanime cache
|
||||
```
|
||||
|
||||
#### update subcommand
|
||||
|
||||
Easily update fastanime to latest
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# update fastanime to latest
|
||||
fastanime update
|
||||
|
||||
# check for latest release
|
||||
fastanime update --check
|
||||
```
|
||||
|
||||
#### completions subcommand
|
||||
|
||||
Helper command to setup shell completions
|
||||
|
||||
**Syntax:**
|
||||
|
||||
```bash
|
||||
# try to detect your shell and print completions
|
||||
fastanime completions
|
||||
# print fish completions
|
||||
fastanime completions --fish
|
||||
# print bash completions
|
||||
fastanime completions --bash
|
||||
# print zsh completions
|
||||
fastanime completions --zsh
|
||||
```
|
||||
|
||||
## MPV specific commands
|
||||
|
||||
The project now allows on the fly media controls directly from mpv. This means you can go to the next or previous episode without the window ever closing thus offering a seamless experience.
|
||||
This is all powered with [python-mpv]() which enables writing mpv scripts with python just like how it would be done in lua.
|
||||
|
||||
### Added keybindings
|
||||
|
||||
`<shift>+n` fetch the next episode
|
||||
|
||||
`<shift>+p` fetch the previous episode
|
||||
|
||||
`<shift>+t` toggle the translation type from dub to sub
|
||||
|
||||
`<shift>+a` toggle auto next episode
|
||||
|
||||
`<shit>+r` reload episode
|
||||
|
||||
### Added script messages
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
# to select episode from mpv without window closing
|
||||
script-message select-episode <episode-number>
|
||||
|
||||
# to select server from mpv without window closing
|
||||
script-message select-server <server-name>
|
||||
|
||||
# to select quality
|
||||
script-message select-quality <1080/720/480/360>
|
||||
```
|
||||
|
||||
## configuration
|
||||
|
||||
The app includes sensible defaults but can be customized extensively. Configuration is stored in `.ini` format at `~/.config/FastAnime/config.ini` on arch linux; for the other operating systems you can check by running `fastanime config --path`.
|
||||
|
||||
```ini
|
||||
[stream]
|
||||
continue_from_history = True # Auto continue from watch history
|
||||
|
||||
# which history to use [local/remote]
|
||||
preferred_history = local
|
||||
|
||||
translation_type = sub # Preferred language for anime (options: dub, sub)
|
||||
|
||||
server = top # Default server (options: dropbox, sharepoint, wetransfer.gogoanime, top, wixmp)
|
||||
|
||||
auto_next = False # Auto-select next episode
|
||||
|
||||
# Auto select the anime provider results with fuzzy find.
|
||||
# Note this wont always be correct.But 99% of the time will be.
|
||||
auto_select=True
|
||||
|
||||
# whether to skip the opening and ending theme songs
|
||||
# note requires ani-skip to be in path
|
||||
skip=false
|
||||
|
||||
# the maximum delta time in minutes after which the episode should be considered as completed
|
||||
# used in the continue from time stamp
|
||||
error=3
|
||||
|
||||
use_mpv_mod=False
|
||||
|
||||
# the format of downloaded anime and trailer
|
||||
# based on yt-dlp format and passed directly to it
|
||||
# learn more by looking it up on their site
|
||||
# only works for downloaded anime if server=gogoanime
|
||||
# since its the only one that offers different formats
|
||||
# the others tend not to
|
||||
format=best[height<=1080]/bestvideo[height<=1080]+bestaudio/best # default
|
||||
|
||||
[general]
|
||||
# can be [allanime,animepahe]
|
||||
provider = allanime
|
||||
|
||||
preferred_language = romaji # Display language (options: english, romaji)
|
||||
|
||||
downloads_dir = <Default-videos-dir>/FastAnime # Download directory
|
||||
|
||||
preview=false # whether to show a preview window when using fzf or rofi
|
||||
|
||||
use_fzf=False # whether to use fzf as the interface for the anilist command and others.
|
||||
|
||||
use_rofi=false # whether to use rofi for the ui
|
||||
|
||||
rofi_theme=<path-to-rofi-theme-file>
|
||||
|
||||
rofi_theme_input=<path-to-rofi-theme-file>
|
||||
|
||||
rofi_theme_confirm=<path-to-rofi-theme-file>
|
||||
|
||||
|
||||
# whether to show the icons
|
||||
icons=false
|
||||
|
||||
# the duration in minutes a notification will stay in the screen
|
||||
# used by notifier command
|
||||
notification_duration=2
|
||||
|
||||
[anilist]
|
||||
# Not implemented yet
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome your issues and feature requests. However, due to time constraints, we currently do not plan to add another provider.
|
||||
|
||||
If you wish to contribute directly, please first open an issue describing your proposed changes so it can be discussed or if you are in a rush for the feature to be merged just open a pr.
|
||||
|
||||
## Receiving Support
|
||||
|
||||
For inquiries, join our [Discord Server](https://discord.gg/4NUTj5Pt).
|
||||
<p align="center">
|
||||
<h1 align="center">FastAnime</h1>
|
||||
</p>
|
||||
<p align="center">
|
||||
<sup>
|
||||
Your browser anime experience, from the terminal.
|
||||
</sup>
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
 
|
||||

|
||||

|
||||

|
||||

|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/C4rhMA4mmK">
|
||||
<a href="https://discord.gg/HBEmAwvbHV">
|
||||
<img src="https://invidget.switchblade.xyz/C4rhMA4mmK">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Supporting the Project
|
||||

|
||||
|
||||
Show your support by starring our GitHub repository or [buying us a coffee](https://ko-fi.com/benex254).
|
||||
<details>
|
||||
<summary>
|
||||
<b>Screenshots</b>
|
||||
</summary>
|
||||
<b>Media Results Menu:</b>
|
||||
<img width="1346" height="710" alt="image" src="https://github.com/user-attachments/assets/c56da5d2-d55d-445c-9ad7-4e007e986d5b" />
|
||||
<b>Episodes Menu with Preview:</b>
|
||||
<img width="1346" height="710" alt="image" src="https://github.com/user-attachments/assets/2294f621-8549-4b1c-9e28-d851b2585037" />
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<b>Riced Preview Examples</b>
|
||||
</summary>
|
||||
|
||||
**Anilist Results Menu (FZF):**
|
||||

|
||||
|
||||
**Episodes Menu with Preview (FZF):**
|
||||

|
||||
|
||||
**No Image Preview Mode:**
|
||||

|
||||
|
||||
**Desktop Notifications + Episodes Menu:**
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## Installation
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
The app runs wherever Python 3.10+ is available. On Android, you can use [Termux](https://github.com/termux/termux-app). For installation help, join our [Discord](https://discord.gg/HBEmAwvbHV).
|
||||
|
||||
### Installation on NixOS
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
nix profile install github:Benexl/fastanime
|
||||
```
|
||||
|
||||
### Installation on Arch Linux
|
||||
|
||||

|
||||
|
||||
Install from the AUR using an AUR helper like `yay` or `paru`.
|
||||
|
||||
```bash
|
||||
# Stable version (recommended)
|
||||
yay -S fastanime
|
||||
|
||||
# Git version (latest commit)
|
||||
yay -S fastanime-git
|
||||
```
|
||||
|
||||
### Recommended Installation (uv)
|
||||
|
||||
The recommended installation method is with [uv](https://docs.astral.sh/uv/), a fast Python package manager.
|
||||
|
||||
```bash
|
||||
# Install with all optional features (recommended for the full experience)
|
||||
uv tool install "fastanime[standard]"
|
||||
|
||||
# Stripped-down installations
|
||||
uv tool install fastanime # Core functionality only
|
||||
uv tool install "fastanime[download]" # For advanced downloading
|
||||
uv tool install "fastanime[discord]" # For Discord Rich Presence
|
||||
uv tool install "fastanime[notifications]" # For desktop notifications
|
||||
```
|
||||
|
||||
### Other Installation Methods
|
||||
|
||||
<details>
|
||||
<summary><b>pipx or pip</b></summary>
|
||||
|
||||
#### Using pipx (Recommended for isolated environments)
|
||||
```bash
|
||||
pipx install "fastanime[standard]"
|
||||
```
|
||||
|
||||
#### Using pip
|
||||
```bash
|
||||
pip install "fastanime[standard]"
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Bleeding Edge & Building from Source</b></summary>
|
||||
|
||||
### Installing the Bleeding Edge Version
|
||||
Download the latest `fastanime_debug_build` artifact from the [GitHub Actions page](https://github.com/Benexl/FastAnime/actions), then:
|
||||
```bash
|
||||
unzip fastanime_debug_build.zip
|
||||
uv tool install fastanime-*.whl
|
||||
```
|
||||
|
||||
### Building from Source
|
||||
Requirements: [Git](https://git-scm.com/), [Python 3.10+](https://www.python.org/), and [uv](https://astral.sh/blog/uv).
|
||||
```bash
|
||||
git clone https://github.com/Benexl/FastAnime.git --depth 1
|
||||
cd FastAnime
|
||||
uv tool install .
|
||||
fastanime --version
|
||||
```
|
||||
</details>
|
||||
|
||||
> [!TIP]
|
||||
> Enable shell completions for a much better experience by running `fastanime completions` and following the on-screen instructions for your shell.
|
||||
|
||||
### External Dependencies
|
||||
|
||||
For the best experience, install these external tools:
|
||||
|
||||
* **Required for Streaming:**
|
||||
* [**mpv**](https://mpv.io/installation/) - The primary media player.
|
||||
* **Recommended for UI & Previews:**
|
||||
* [**fzf**](https://github.com/junegunn/fzf) - For a powerful fuzzy-finder interface.
|
||||
* [**chafa**](https://github.com/hpjansson/chafa) or [**kitty's icat**](https://sw.kovidgoyal.net/kitty/kittens/icat/) - For image previews in the terminal.
|
||||
* **Recommended for Downloads & Features:**
|
||||
* [**ffmpeg**](https://www.ffmpeg.org/) - Required for downloading HLS streams.
|
||||
* [**webtorrent-cli**](https://github.com/webtorrent/webtorrent-cli) - For streaming torrents.
|
||||
* [**syncplay**](https://syncplay.pl/) - To watch anime together with friends.
|
||||
* [**feh**](https://github.com/derf/feh) or **kitty's icat** - For the experimental manga mode.
|
||||
|
||||
## Usage
|
||||
|
||||
FastAnime offers a rich interactive TUI for browsing and a powerful CLI for scripting and automation.
|
||||
|
||||
### Global Options
|
||||
|
||||
Most options can be passed directly to the `fastanime` command to override your config for that session.
|
||||
|
||||
* `--provider <allanime|animepahe>`: Choose the streaming site to use.
|
||||
* `--selector <fzf|rofi|default>`: Choose the UI backend.
|
||||
* `--preview`, `--no-preview`: Enable/disable image and info previews (requires `fzf`).
|
||||
* `--dub`, `--sub`: Set preferred translation type.
|
||||
* `--icons`, `--no-icons`: Toggle UI icons.
|
||||
* `--log`, `--log-file`: Enable logging to stdout or a file for debugging.
|
||||
* `--rich-traceback`: Show detailed, formatted tracebacks on error.
|
||||
|
||||
### Main Commands
|
||||
|
||||
* `fastanime anilist`: The main entry point for the interactive TUI. Browse, search, and manage your lists.
|
||||
* `fastanime registry`: Manage your local database of anime. Sync, search, backup, and restore.
|
||||
* `fastanime download`: Scriptable command to download specific episodes.
|
||||
* `fastanime search`: Scriptable command to find and stream episodes directly.
|
||||
* `fastanime config`: Manage your configuration file.
|
||||
* `fastanime update`: Update FastAnime to the latest version.
|
||||
* `fastanime queue`: Add episodes to the background download queue.
|
||||
* `fastanime worker`: Run the background worker for downloads and notifications.
|
||||
|
||||
---
|
||||
|
||||
### Deep Dive: `fastanime anilist` (Interactive TUI)
|
||||
|
||||
This is the primary way to use FastAnime. Simply run `fastanime anilist` to launch a rich, interactive terminal experience. From here you can:
|
||||
|
||||
* Browse trending, popular, and seasonal anime.
|
||||
* Manage your personal lists (Watching, Completed, etc.) after logging in with `fastanime anilist auth`.
|
||||
* Search for any anime in the AniList database.
|
||||
* View detailed information, characters, recommendations, reviews, and airing schedules.
|
||||
* Stream or download episodes.
|
||||
|
||||
#### `anilist search` Subcommand
|
||||
|
||||
A powerful command to filter the AniList database directly from your terminal.
|
||||
|
||||
```bash
|
||||
# Search for anime from 2024, sorted by popularity, that is releasing and not on your list
|
||||
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --not-on-list
|
||||
|
||||
# Find the most popular movies with the "Fantasy" genre
|
||||
fastanime anilist search -g Fantasy -f MOVIE -s POPULARITY_DESC
|
||||
|
||||
# Dump search results as JSON instead of launching the TUI
|
||||
fastanime anilist search -t "Demon Slayer" --dump-json
|
||||
```
|
||||
|
||||
#### `anilist download` Subcommand
|
||||
|
||||
Combines the power of `anilist search` with the `download` command, allowing you to batch-download based on filters.
|
||||
|
||||
```bash
|
||||
# Download episodes 1-12 of all fantasy anime that aired in Winter 2024
|
||||
fastanime anilist download --season WINTER -y 2024 -g Fantasy -r "0:12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Deep Dive: `fastanime registry` (Local Database)
|
||||
|
||||
FastAnime maintains a local registry of your anime for offline access, enhanced performance, and powerful data management.
|
||||
|
||||
* `registry sync`: Synchronize your local data with your remote AniList account.
|
||||
* `registry stats`: Show detailed statistics about your viewing habits.
|
||||
* `registry search`: Search your locally stored anime data.
|
||||
* `registry backup`: Create a compressed backup of your entire registry.
|
||||
* `registry restore`: Restore your data from a backup file.
|
||||
* `registry export/import`: Export your data to JSON/CSV for use in other applications.
|
||||
|
||||
---
|
||||
|
||||
### Scriptable Commands: `download` & `search`
|
||||
|
||||
These commands are designed for automation and quick access.
|
||||
|
||||
#### `download` Examples
|
||||
```bash
|
||||
# Download the latest 5 episodes of One Piece
|
||||
fastanime download -t "One Piece" -r "-5"
|
||||
|
||||
# Download episodes 1 to 24, merge subtitles, and clean up original files
|
||||
fastanime download -t "Jujutsu Kaisen" -r "0:24" --merge --clean
|
||||
```
|
||||
|
||||
#### `search` (Binging) Examples
|
||||
```bash
|
||||
# Start binging an anime from the first episode
|
||||
fastanime search -t "Attack on Titan" -r ":"
|
||||
|
||||
# Watch the latest episode directly
|
||||
fastanime search -t "My Hero Academia" -r "-1"
|
||||
```
|
||||
---
|
||||
### MPV IPC Integration
|
||||
|
||||
When `use_ipc` is enabled, FastAnime provides powerful in-player controls without closing MPV.
|
||||
|
||||
#### Key Bindings
|
||||
* `Shift+N`: Play the next episode.
|
||||
* `Shift+P`: Play the previous episode.
|
||||
* `Shift+R`: Reload the current episode.
|
||||
* `Shift+A`: Toggle auto-play for the next episode.
|
||||
* `Shift+T`: Toggle between `dub` and `sub`.
|
||||
|
||||
#### Script Messages (MPV Console)
|
||||
* `script-message select-episode <number>`: Jump to a specific episode.
|
||||
* `script-message select-server <name>`: Switch to a different streaming server.
|
||||
|
||||
## Configuration
|
||||
|
||||
FastAnime is highly customizable via its configuration file, located at `~/.config/fastanime/config.ini` (path may vary by OS).
|
||||
Run `fastanime config --path` to find the exact location on your system.
|
||||
|
||||
A default configuration file with detailed comments is created on first run. You can edit it with `fastanime config` or use the interactive wizard with `fastanime config --interactive`.
|
||||
|
||||
<details>
|
||||
<summary><b>Default Configuration (`config.ini`)</b></summary>
|
||||
|
||||
```ini
|
||||
[general]
|
||||
# The preferred watch history tracker (local,remote) in cases of conflicts
|
||||
preferred_tracker = local
|
||||
# The pygment style to use
|
||||
pygment_style = github-dark
|
||||
# The spinner to use
|
||||
preferred_spinner = smiley
|
||||
# The media database API to use (e.g., 'anilist', 'jikan').
|
||||
media_api = anilist
|
||||
# The default anime provider to use for scraping.
|
||||
provider = allanime
|
||||
# The interactive selector tool to use for menus.
|
||||
selector = fzf
|
||||
# Automatically select the best-matching search result from a provider.
|
||||
auto_select_anime_result = True
|
||||
# Display emoji icons in the user interface.
|
||||
icons = True
|
||||
# Type of preview to display in selectors.
|
||||
preview = full
|
||||
# The command-line tool to use for rendering images in the terminal.
|
||||
image_renderer = icat
|
||||
# The external application to use for viewing manga pages.
|
||||
manga_viewer = feh
|
||||
# Automatically check for new versions of FastAnime on startup.
|
||||
check_for_updates = True
|
||||
# Enable caching of network requests to speed up subsequent operations.
|
||||
cache_requests = True
|
||||
# Maximum lifetime for a cached request in DD:HH:MM format.
|
||||
max_cache_lifetime = 03:00:00
|
||||
# Attempt to normalize provider titles to match AniList titles.
|
||||
normalize_titles = True
|
||||
# Enable Discord Rich Presence to show your current activity.
|
||||
discord = False
|
||||
# Number of recently watched anime to keep in history.
|
||||
recent = 50
|
||||
|
||||
[stream]
|
||||
# The media player to use for streaming.
|
||||
player = mpv
|
||||
# Preferred stream quality.
|
||||
quality = 1080
|
||||
# Preferred audio/subtitle language type.
|
||||
translation_type = sub
|
||||
# The default server to use from a provider. 'top' uses the first available.
|
||||
server = TOP
|
||||
# Automatically play the next episode when the current one finishes.
|
||||
auto_next = False
|
||||
# Automatically resume playback from the last known episode and position.
|
||||
continue_from_watch_history = True
|
||||
# Which watch history to prioritize: local file or remote AniList progress.
|
||||
preferred_watch_history = local
|
||||
# Automatically skip openings/endings if skip data is available.
|
||||
auto_skip = False
|
||||
# Percentage of an episode to watch before it's marked as complete.
|
||||
episode_complete_at = 80
|
||||
# The format selection string for yt-dlp.
|
||||
ytdlp_format = best[height<=1080]/bestvideo[height<=1080]+bestaudio/best
|
||||
# Prevent updating AniList progress to a lower episode number.
|
||||
force_forward_tracking = True
|
||||
# Default behavior for tracking progress on AniList.
|
||||
default_media_list_tracking = prompt
|
||||
# Preferred language code for subtitles (e.g., 'en', 'es').
|
||||
sub_lang = eng
|
||||
# Use IPC communication with the player for advanced features like episode navigation.
|
||||
use_ipc = True
|
||||
```
|
||||
</details>
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are highly welcome! Please read our [**Contributing Guidelines**](CONTRIBUTIONS.md) to get started with setting up a development environment and understanding our coding standards.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> This project scrapes public-facing websites (`allanime`, `animepahe`). The developer(s) of this application have no affiliation with these content providers. This application hosts zero content. Use at your own risk.
|
||||
>
|
||||
> [**Full Disclaimer**](DISCLAIMER.md)
|
||||
|
||||
7
bundle/Dockerfile
Normal file
7
bundle/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM python:3.12-slim-bookworm
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
COPY . /fastanime
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
WORKDIR /fastanime
|
||||
RUN uv tool install .
|
||||
CMD ["bash"]
|
||||
65
bundle/pyinstaller.spec
Normal file
65
bundle/pyinstaller.spec
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# Collect all required data files
|
||||
datas = [
|
||||
('fastanime/assets/*', 'fastanime/assets'),
|
||||
]
|
||||
|
||||
# Collect all required hidden imports
|
||||
hiddenimports = [
|
||||
'click',
|
||||
'rich',
|
||||
'requests',
|
||||
'yt_dlp',
|
||||
'python_mpv',
|
||||
'fuzzywuzzy',
|
||||
'fastanime',
|
||||
] + collect_submodules('fastanime')
|
||||
|
||||
a = Analysis(
|
||||
['./fastanime/fastanime.py'], # Changed entry point
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
strip=True, # Strip debug information
|
||||
optimize=2 # Optimize bytecode noarchive=False
|
||||
)
|
||||
|
||||
pyz = PYZ(
|
||||
a.pure,
|
||||
a.zipped_data,
|
||||
optimize=2 # Optimize bytecode cipher=block_cipher
|
||||
)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='fastanime',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='fastanime/assets/logo.ico'
|
||||
)
|
||||
2460
dev/generated/anilist/tags.json
Normal file
2460
dev/generated/anilist/tags.json
Normal file
File diff suppressed because it is too large
Load Diff
16
dev/make_release
Executable file
16
dev/make_release
Executable file
@@ -0,0 +1,16 @@
|
||||
#! /usr/bin/env sh
|
||||
CLI_DIR="$(dirname "$(realpath "$0")")"
|
||||
VERSION=$1
|
||||
[ -z "$VERSION" ] && echo no version provided && exit 1
|
||||
[ "$VERSION" = "current" ] && fastanime --version && exit 0
|
||||
sed -i "s/^version.*/version = \"$VERSION\"/" "$CLI_DIR/pyproject.toml" &&
|
||||
sed -i "s/__version__.*/__version__ = \"v$VERSION\"/" "$CLI_DIR/fastanime/__init__.py" &&
|
||||
sed -i "s/version = .*/version = \"$VERSION\";/" "$CLI_DIR/flake.nix" &&
|
||||
git stage "$CLI_DIR/pyproject.toml" "$CLI_DIR/fastanime/__init__.py" "$CLI_DIR/flake.nix" &&
|
||||
git commit -m "chore: bump version (v$VERSION)" &&
|
||||
# nix flake lock &&
|
||||
uv lock &&
|
||||
git stage "$CLI_DIR/flake.lock" "$CLI_DIR/uv.lock" &&
|
||||
git commit -m "chore: update lock files" &&
|
||||
git push &&
|
||||
gh release create "v$VERSION"
|
||||
8
fa
8
fa
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
# exec "${PYTHON:-python3}" -Werror -Xdev -m "$(dirname "$(realpath "$0")")/fastanime" "$@"
|
||||
cd "$(dirname "$(realpath "$0")")" || exit 1
|
||||
exec python -m fastanime "$@"
|
||||
provider_type=$1
|
||||
provider_name=$2
|
||||
[ -z "$provider_type" ] && echo "Please specify provider type" && exit
|
||||
[ -z "$provider_name" ] && echo "Please specify provider type" && exit
|
||||
uv run python -m fastanime.libs.provider.${provider_type}.${provider_name}.provider
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
"""An abstraction over all providers offering added features with a simple and well typed api
|
||||
|
||||
[TODO:description]
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .libs.anime_provider import anime_sources
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterator
|
||||
|
||||
from .libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
from .libs.anime_provider.types import Anime, SearchResults, Server
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: improve performance of this class and add cool features like auto retry
|
||||
class AnimeProvider:
|
||||
"""Class that manages all anime sources adding some extra functionality to them.
|
||||
Attributes:
|
||||
PROVIDERS: [TODO:attribute]
|
||||
provider: [TODO:attribute]
|
||||
provider: [TODO:attribute]
|
||||
dynamic: [TODO:attribute]
|
||||
retries: [TODO:attribute]
|
||||
anime_provider: [TODO:attribute]
|
||||
"""
|
||||
|
||||
PROVIDERS = list(anime_sources.keys())
|
||||
provider = PROVIDERS[0]
|
||||
|
||||
def __init__(self, provider, dynamic=False, retries=0) -> None:
|
||||
self.provider = provider
|
||||
self.dynamic = dynamic
|
||||
self.retries = retries
|
||||
self.lazyload_provider()
|
||||
|
||||
def lazyload_provider(self):
|
||||
"""updates the current provider being used"""
|
||||
_, anime_provider_cls_name = anime_sources[self.provider].split(".", 1)
|
||||
package = f"fastanime.libs.anime_provider.{self.provider}"
|
||||
provider_api = importlib.import_module(".api", package)
|
||||
anime_provider = getattr(provider_api, anime_provider_cls_name)
|
||||
self.anime_provider = anime_provider()
|
||||
|
||||
def search_for_anime(
|
||||
self,
|
||||
user_query,
|
||||
translation_type,
|
||||
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
|
||||
nsfw=True,
|
||||
unknown=True,
|
||||
) -> "SearchResults | None":
|
||||
"""core abstraction over all providers search functionality
|
||||
|
||||
Args:
|
||||
user_query ([TODO:parameter]): [TODO:description]
|
||||
translation_type ([TODO:parameter]): [TODO:description]
|
||||
nsfw ([TODO:parameter]): [TODO:description]
|
||||
unknown ([TODO:parameter]): [TODO:description]
|
||||
anilist_obj: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
try:
|
||||
results = anime_provider.search_for_anime(
|
||||
user_query, translation_type, nsfw, unknown
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
results = None
|
||||
return results
|
||||
|
||||
def get_anime(
|
||||
self,
|
||||
anime_id: str,
|
||||
anilist_obj: "AnilistBaseMediaDataSchema | None" = None,
|
||||
) -> "Anime | None":
|
||||
"""core abstraction over getting info of an anime from all providers
|
||||
|
||||
Args:
|
||||
anime_id: [TODO:description]
|
||||
anilist_obj: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
try:
|
||||
results = anime_provider.get_anime(anime_id)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
results = None
|
||||
return results
|
||||
|
||||
def get_episode_streams(
|
||||
self,
|
||||
anime,
|
||||
episode: str,
|
||||
translation_type: str,
|
||||
anilist_obj: "AnilistBaseMediaDataSchema|None" = None,
|
||||
) -> "Iterator[Server] | None":
|
||||
"""core abstractions for getting juicy streams from all providers
|
||||
|
||||
Args:
|
||||
anime ([TODO:parameter]): [TODO:description]
|
||||
episode: [TODO:description]
|
||||
translation_type: [TODO:description]
|
||||
anilist_obj: [TODO:description]
|
||||
|
||||
Returns:
|
||||
[TODO:return]
|
||||
"""
|
||||
anime_provider = self.anime_provider
|
||||
try:
|
||||
results = anime_provider.get_episode_streams(
|
||||
anime, episode, translation_type
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
results = None
|
||||
return results # pyright:ignore
|
||||
@@ -1,4 +0,0 @@
|
||||
"""This package exist as away to expose functions and classes that my be useful to a developer using the fastanime library
|
||||
|
||||
[TODO:description]
|
||||
"""
|
||||
@@ -1,34 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistDateObject, AnilistMediaNextAiringEpisode
|
||||
|
||||
|
||||
# TODO: Add formating options for the final date
|
||||
def format_anilist_date_object(anilist_date_object: "AnilistDateObject"):
|
||||
if anilist_date_object:
|
||||
return f"{anilist_date_object['day']}/{anilist_date_object['month']}/{anilist_date_object['year']}"
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def format_anilist_timestamp(anilist_timestamp: int | None):
|
||||
if anilist_timestamp:
|
||||
return datetime.fromtimestamp(anilist_timestamp).strftime("%d/%m/%Y %H:%M:%S")
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def format_list_data_with_comma(data: list | None):
|
||||
if data:
|
||||
return ", ".join(data)
|
||||
else:
|
||||
return "None"
|
||||
|
||||
|
||||
def extract_next_airing_episode(airing_episode: "AnilistMediaNextAiringEpisode"):
|
||||
if airing_episode:
|
||||
return f"{airing_episode['episode']} on {format_anilist_timestamp(airing_episode['airingAt'])}"
|
||||
else:
|
||||
return "Completed"
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
Just contains some useful data used across the codebase
|
||||
"""
|
||||
|
||||
# useful incases where the anilist title is too different from the provider title
|
||||
anime_normalizer = {
|
||||
"1P": "one piece",
|
||||
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
||||
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
||||
'Hazurewaku no "Joutai Ijou Skill" de Saikyou ni Natta Ore ga Subete wo Juurin suru made': "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
||||
}
|
||||
|
||||
|
||||
anilist_sort_normalizer = {"search match": "SEARCH_MATCH"}
|
||||
@@ -1,74 +0,0 @@
|
||||
import logging
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
import yt_dlp
|
||||
from yt_dlp.utils import sanitize_filename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class YtDLPDownloader:
|
||||
downloads_queue = Queue()
|
||||
|
||||
def _worker(self):
|
||||
while True:
|
||||
task, args = self.downloads_queue.get()
|
||||
try:
|
||||
task(*args)
|
||||
except Exception as e:
|
||||
logger.error(f"Something went wrong {e}")
|
||||
self.downloads_queue.task_done()
|
||||
|
||||
def __init__(self):
|
||||
self._thread = Thread(target=self._worker)
|
||||
self._thread.daemon = True
|
||||
self._thread.start()
|
||||
|
||||
# Function to download the file
|
||||
# TODO: untpack the title to its actual values episode_title and anime_title
|
||||
def _download_file(
|
||||
self,
|
||||
url: str,
|
||||
anime_title: str,
|
||||
episode_title: str,
|
||||
download_dir: str,
|
||||
silent: bool,
|
||||
vid_format: str = "best",
|
||||
):
|
||||
"""Helper function that downloads anime given url and path details
|
||||
|
||||
Args:
|
||||
url: [TODO:description]
|
||||
anime_title: [TODO:description]
|
||||
episode_title: [TODO:description]
|
||||
download_dir: [TODO:description]
|
||||
silent: [TODO:description]
|
||||
vid_format: [TODO:description]
|
||||
"""
|
||||
anime_title = sanitize_filename(anime_title)
|
||||
episode_title = sanitize_filename(episode_title)
|
||||
ydl_opts = {
|
||||
# Specify the output path and template
|
||||
"outtmpl": f"{download_dir}/{anime_title}/{episode_title}.%(ext)s",
|
||||
"silent": silent,
|
||||
"verbose": False,
|
||||
"format": vid_format,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
# WARN: May remove this legacy functionality
|
||||
def download_file(self, url: str, title, silent=True):
|
||||
"""A helper that just does things in the background
|
||||
|
||||
Args:
|
||||
title ([TODO:parameter]): [TODO:description]
|
||||
silent ([TODO:parameter]): [TODO:description]
|
||||
url: [TODO:description]
|
||||
"""
|
||||
self.downloads_queue.put((self._download_file, (url, title, silent)))
|
||||
|
||||
|
||||
downloader = YtDLPDownloader()
|
||||
@@ -1,38 +0,0 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from thefuzz import fuzz
|
||||
|
||||
from .data import anime_normalizer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..libs.anilist.types import AnilistBaseMediaDataSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def anime_title_percentage_match(
|
||||
possible_user_requested_anime_title: str, anime: "AnilistBaseMediaDataSchema"
|
||||
) -> float:
|
||||
"""Returns the percentage match between the possible title and user title
|
||||
|
||||
Args:
|
||||
possible_user_requested_anime_title (str): an Animdl search result title
|
||||
title (str): the anime title the user wants
|
||||
|
||||
Returns:
|
||||
int: the percentage match
|
||||
"""
|
||||
if normalized_anime_title := anime_normalizer.get(
|
||||
possible_user_requested_anime_title
|
||||
):
|
||||
possible_user_requested_anime_title = normalized_anime_title
|
||||
# compares both the romaji and english names and gets highest Score
|
||||
title_a = str(anime["title"]["romaji"])
|
||||
title_b = str(anime["title"]["english"])
|
||||
percentage_ratio = max(
|
||||
fuzz.ratio(title_a.lower(), possible_user_requested_anime_title.lower()),
|
||||
fuzz.ratio(title_b.lower(), possible_user_requested_anime_title.lower()),
|
||||
)
|
||||
logger.info(f"{locals()}")
|
||||
return percentage_ratio
|
||||
@@ -2,19 +2,11 @@ import sys
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
"You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp"
|
||||
"You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by FastAnime"
|
||||
) # noqa: F541
|
||||
|
||||
|
||||
__version__ = "v2.0.0"
|
||||
|
||||
APP_NAME = "FastAnime"
|
||||
AUTHOR = "Benex254"
|
||||
GIT_REPO = "github.com"
|
||||
REPO = f"{GIT_REPO}/{AUTHOR}/{APP_NAME}"
|
||||
|
||||
|
||||
def FastAnime():
|
||||
def Cli():
|
||||
from .cli import run_cli
|
||||
|
||||
run_cli()
|
||||
|
||||
@@ -9,6 +9,6 @@ if __package__ is None and not getattr(sys, "frozen", False):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from . import FastAnime
|
||||
from . import Cli
|
||||
|
||||
FastAnime()
|
||||
Cli()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .libs.anilist.api import AniListApi
|
||||
|
||||
AniList = AniListApi()
|
||||
6
fastanime/assets/defaults/ascii-art
Normal file
6
fastanime/assets/defaults/ascii-art
Normal file
@@ -0,0 +1,6 @@
|
||||
███████╗░█████╗░░██████╗████████╗░█████╗░███╗░░██╗██╗███╗░░░███╗███████╗
|
||||
██╔════╝██╔══██╗██╔════╝╚══██╔══╝██╔══██╗████╗░██║██║████╗░████║██╔════╝
|
||||
█████╗░░███████║╚█████╗░░░░██║░░░███████║██╔██╗██║██║██╔████╔██║█████╗░░
|
||||
██╔══╝░░██╔══██║░╚═══██╗░░░██║░░░██╔══██║██║╚████║██║██║╚██╔╝██║██╔══╝░░
|
||||
██║░░░░░██║░░██║██████╔╝░░░██║░░░██║░░██║██║░╚███║██║██║░╚═╝░██║███████╗
|
||||
╚═╝░░░░░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚══╝╚═╝╚═╝░░░░░╚═╝╚══════╝
|
||||
16
fastanime/assets/defaults/fastanime-worker.template.service
Normal file
16
fastanime/assets/defaults/fastanime-worker.template.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# values in {NAME} syntax are provided by python using .replace()
|
||||
#
|
||||
[Unit]
|
||||
Description=FastAnime Background Worker
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Ensure you have the full path to your fastanime executable
|
||||
# Use `which fastanime` to find it
|
||||
ExecStart={EXECUTABLE} worker --log
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
23
fastanime/assets/defaults/fzf-opts
Normal file
23
fastanime/assets/defaults/fzf-opts
Normal file
@@ -0,0 +1,23 @@
|
||||
--color=fg:#d0d0d0,fg+:#d0d0d0,bg:#121212,bg+:#262626
|
||||
--color=hl:#5f87af,hl+:#5fd7ff,info:#afaf87,marker:#87ff00
|
||||
--color=prompt:#d7005f,spinner:#af5fff,pointer:#af5fff,header:#87afaf
|
||||
--color=border:#262626,label:#aeaeae,query:#d9d9d9
|
||||
--border=rounded
|
||||
--border-label=''
|
||||
--prompt='>'
|
||||
--marker='>'
|
||||
--pointer='◆'
|
||||
--separator='─'
|
||||
--scrollbar='│'
|
||||
--layout=reverse
|
||||
--cycle
|
||||
--info=hidden
|
||||
--height=100%
|
||||
--bind=right:accept,ctrl-/:toggle-preview,ctrl-space:toggle-wrap+toggle-preview-wrap
|
||||
--no-margin
|
||||
+m
|
||||
-i
|
||||
--exact
|
||||
--tabstop=1
|
||||
--preview-window=border-rounded,left,35%,wrap
|
||||
--wrap
|
||||
55
fastanime/assets/defaults/rofi-themes/confirm.rasi
Normal file
55
fastanime/assets/defaults/rofi-themes/confirm.rasi
Normal file
@@ -0,0 +1,55 @@
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
}
|
||||
|
||||
* {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
text-color: #FFFFFF;
|
||||
}
|
||||
|
||||
window {
|
||||
fullscreen: true;
|
||||
transparency: "real";
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [ message, listview, inputbar ];
|
||||
padding: 40% 30%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
message {
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
border-radius:20px;
|
||||
margin: 0 0 20px 0;
|
||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
prompt {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
listview {
|
||||
lines: 0;
|
||||
}
|
||||
|
||||
/* Style for the message text specifically */
|
||||
textbox {
|
||||
horizontal-align: 0.5; /* Center the text */
|
||||
font: "Sans Bold 24"; /* Match message font */
|
||||
background-color: transparent;
|
||||
}
|
||||
55
fastanime/assets/defaults/rofi-themes/input.rasi
Normal file
55
fastanime/assets/defaults/rofi-themes/input.rasi
Normal file
@@ -0,0 +1,55 @@
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
}
|
||||
|
||||
* {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
text-color: #FFFFFF;
|
||||
}
|
||||
|
||||
window {
|
||||
fullscreen: true;
|
||||
transparency: "real";
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [ message, listview, inputbar ];
|
||||
padding: 40% 30%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
message {
|
||||
border: 0;
|
||||
padding: 10px;
|
||||
border-radius:20px;
|
||||
margin: 0 0 20px 0;
|
||||
font: "Sans Bold 24"; /* Increased font size and made it bold */
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
prompt {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
listview {
|
||||
lines: 0;
|
||||
}
|
||||
|
||||
/* Style for the message text specifically */
|
||||
textbox {
|
||||
horizontal-align: 0.5; /* Center the text */
|
||||
font: "Sans Bold 24"; /* Match message font */
|
||||
background-color: transparent;
|
||||
}
|
||||
80
fastanime/assets/defaults/rofi-themes/main.rasi
Normal file
80
fastanime/assets/defaults/rofi-themes/main.rasi
Normal file
@@ -0,0 +1,80 @@
|
||||
configuration {
|
||||
font: "Sans 12";
|
||||
line-margin: 10;
|
||||
display-drun: "";
|
||||
}
|
||||
|
||||
* {
|
||||
background: #000000; /* Black background for everything */
|
||||
background-alt: #000000; /* Ensures no alternation */
|
||||
foreground: #CCCCCC;
|
||||
selected: #3584E4;
|
||||
active: #2E7D32;
|
||||
urgent: #C62828;
|
||||
}
|
||||
|
||||
window {
|
||||
fullscreen: false;
|
||||
background-color: rgba(0, 0, 0, 0.8); /* Solid black transparent background */
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
padding: 50px 50px;
|
||||
background-color: transparent; /* Ensures black background fills entire main area */
|
||||
children: [inputbar, listview];
|
||||
spacing: 20px;
|
||||
}
|
||||
|
||||
inputbar {
|
||||
background-color: #333333; /* Dark gray background for input bar */
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
children: [prompt, entry];
|
||||
}
|
||||
|
||||
prompt {
|
||||
enabled: true;
|
||||
padding: 8px;
|
||||
background-color: @selected;
|
||||
text-color: #000000;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
entry {
|
||||
padding: 8px;
|
||||
background-color: transparent; /* Slightly lighter gray for visibility */
|
||||
text-color: #FFFFFF; /* White text to make typing visible */
|
||||
placeholder: "Search...";
|
||||
placeholder-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
listview {
|
||||
layout: vertical;
|
||||
spacing: 8px;
|
||||
lines: 9;
|
||||
background-color: transparent; /* Consistent black background for list items */
|
||||
}
|
||||
|
||||
element {
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: transparent; /* Uniform color for each list item */
|
||||
text-color: @foreground;
|
||||
}
|
||||
|
||||
element normal.normal {
|
||||
background-color: transparent; /* Ensures no alternating color */
|
||||
}
|
||||
|
||||
element selected.normal {
|
||||
background-color: @selected;
|
||||
text-color: #FFFFFF;
|
||||
}
|
||||
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
120
fastanime/assets/defaults/rofi-themes/preview.rasi
Normal file
120
fastanime/assets/defaults/rofi-themes/preview.rasi
Normal file
@@ -0,0 +1,120 @@
|
||||
// Colours
|
||||
* {
|
||||
background-color: transparent; /* Transparent background for the global UI */
|
||||
background: #000000; /* Solid black background */
|
||||
background-transparent: #1D2330A0; /* Semi-transparent background */
|
||||
text-color: #BBBBBB; /* Default text color (light gray) */
|
||||
text-color-selected: #FFFFFF; /* Text color when selected (white) */
|
||||
primary: rgba(53, 132, 228, 0.75); /* Blusish primary color */
|
||||
important: rgba(53, 132, 228, 0.75); /* Bluish primary color */
|
||||
}
|
||||
|
||||
configuration {
|
||||
font: "Roboto 14"; /* Sets the global font to Roboto, size 14 */
|
||||
show-icons: true; /* Option to display icons in the UI */
|
||||
}
|
||||
|
||||
window {
|
||||
fullscreen: true; /* The window will open in fullscreen */
|
||||
height: 100%; /* Full window height */
|
||||
width: 100%; /* Full window width */
|
||||
transparency: "real"; /* Real transparency effect */
|
||||
background-color: @background-transparent; /* Transparent background */
|
||||
border: 0px; /* No border around the window */
|
||||
border-color: @primary; /* Border color set to the primary color */
|
||||
}
|
||||
|
||||
mainbox {
|
||||
children: [prompt, inputbar-box, listview]; /* Main box contains prompt, input bar, and list view */
|
||||
padding: 0px; /* No padding around the main box */
|
||||
}
|
||||
|
||||
prompt {
|
||||
width: 100%; /* Prompt takes full width */
|
||||
margin: 10px 0px 0px 30px; /* Margin around the prompt */
|
||||
text-color: @important; /* Text color for prompt (important color) */
|
||||
font: "Roboto Bold 27"; /* Bold Roboto font, size 27 */
|
||||
}
|
||||
|
||||
listview {
|
||||
layout: vertical; /* Vertical layout for list items */
|
||||
padding: 10px; /* Padding inside the list view */
|
||||
spacing: 20px; /* Space between items in the list */
|
||||
columns: 8; /* Maximum 8 items per row */
|
||||
dynamic: true; /* Allows the list to dynamically adjust */
|
||||
orientation: horizontal; /* Horizontal orientation for list items */
|
||||
}
|
||||
|
||||
inputbar-box {
|
||||
children: [dummy, inputbar, dummy]; /* Input bar is centered with dummy placeholders */
|
||||
orientation: horizontal; /* Horizontal layout for input bar */
|
||||
expand: false; /* Does not expand to fill the space */
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [textbox-prompt, entry]; /* Contains a prompt and an entry field */
|
||||
margin: 0px; /* No margin around the input bar */
|
||||
background-color: @primary; /* Background color set to the primary color */
|
||||
border: 4px; /* Border thickness around the input bar */
|
||||
border-color: @primary; /* Border color matches the primary color */
|
||||
border-radius: 8px; /* Rounded corners for the input bar */
|
||||
}
|
||||
|
||||
textbox-prompt {
|
||||
text-color: @background; /* Text color inside prompt matches the background color */
|
||||
horizontal-align: 0.5; /* Horizontally centered */
|
||||
vertical-align: 0.5; /* Vertically centered */
|
||||
expand: false; /* Does not expand to fill available space */
|
||||
}
|
||||
|
||||
entry {
|
||||
expand: false; /* Entry field does not expand */
|
||||
padding: 8px; /* Padding inside the entry field */
|
||||
margin: -6px; /* Negative margin to position entry properly */
|
||||
horizontal-align: 0; /* Left-aligned text inside the entry field */
|
||||
width: 300; /* Fixed width for the entry field */
|
||||
background-color: @background; /* Entry background color matches the global background */
|
||||
border: 6px; /* Border thickness around the entry field */
|
||||
border-color: @primary; /* Border color matches the primary color */
|
||||
border-radius: 8px; /* Rounded corners for the entry field */
|
||||
cursor: text; /* Cursor changes to text input cursor inside the entry field */
|
||||
}
|
||||
|
||||
element {
|
||||
children: [dummy, element-box, dummy]; /* Contains an element box with dummy placeholders */
|
||||
padding: 5px; /* Padding around the element */
|
||||
orientation: vertical; /* Vertical layout for element content */
|
||||
border: 0px; /* No border around the element */
|
||||
border-radius: 16px; /* Rounded corners for the element */
|
||||
background-color: transparent; /* Transparent background */
|
||||
width: 100px; /* Width of each element */
|
||||
height: 50px; /* Height of each element */
|
||||
}
|
||||
|
||||
element selected {
|
||||
background-color: @primary; /* Background color of the element when selected */
|
||||
}
|
||||
|
||||
element-box {
|
||||
children: [element-icon, element-text]; /* Element box contains an icon and text */
|
||||
orientation: vertical; /* Vertical layout for icon and text */
|
||||
expand: false; /* Does not expand to fill available space */
|
||||
cursor: pointer; /* Cursor changes to a pointer when hovering over the element */
|
||||
}
|
||||
|
||||
element-icon {
|
||||
padding: 10px; /* Padding inside the icon */
|
||||
cursor: inherit; /* Inherits cursor style from the parent */
|
||||
size: 33%; /* Icon size is set to 33% of the parent element */
|
||||
margin: 10px; /* Margin around the icon */
|
||||
}
|
||||
|
||||
element-text {
|
||||
horizontal-align: 0.5; /* Horizontally center-aligns the text */
|
||||
cursor: inherit; /* Inherits cursor style from the parent */
|
||||
text-color: @text-color; /* Text color for element text */
|
||||
}
|
||||
|
||||
element-text selected {
|
||||
text-color: @text-color-selected; /* Text color when the element is selected */
|
||||
}
|
||||
7
fastanime/assets/graphql/allanime/queries/anime.gql
Normal file
7
fastanime/assets/graphql/allanime/queries/anime.gql
Normal file
@@ -0,0 +1,7 @@
|
||||
query ($showId: String!) {
|
||||
show(_id: $showId) {
|
||||
_id
|
||||
name
|
||||
availableEpisodesDetail
|
||||
}
|
||||
}
|
||||
15
fastanime/assets/graphql/allanime/queries/episodes.gql
Normal file
15
fastanime/assets/graphql/allanime/queries/episodes.gql
Normal file
@@ -0,0 +1,15 @@
|
||||
query (
|
||||
$showId: String!
|
||||
$translationType: VaildTranslationTypeEnumType!
|
||||
$episodeString: String!
|
||||
) {
|
||||
episode(
|
||||
showId: $showId
|
||||
translationType: $translationType
|
||||
episodeString: $episodeString
|
||||
) {
|
||||
episodeString
|
||||
sourceUrls
|
||||
notes
|
||||
}
|
||||
}
|
||||
25
fastanime/assets/graphql/allanime/queries/search.gql
Normal file
25
fastanime/assets/graphql/allanime/queries/search.gql
Normal file
@@ -0,0 +1,25 @@
|
||||
query (
|
||||
$search: SearchInput
|
||||
$limit: Int
|
||||
$page: Int
|
||||
$translationType: VaildTranslationTypeEnumType
|
||||
$countryOrigin: VaildCountryOriginEnumType
|
||||
) {
|
||||
shows(
|
||||
search: $search
|
||||
limit: $limit
|
||||
page: $page
|
||||
translationType: $translationType
|
||||
countryOrigin: $countryOrigin
|
||||
) {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
edges {
|
||||
_id
|
||||
name
|
||||
availableEpisodes
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation ($id: Int) {
|
||||
DeleteMediaListEntry(id: $id) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
5
fastanime/assets/graphql/anilist/mutations/mark-read.gql
Normal file
5
fastanime/assets/graphql/anilist/mutations/mark-read.gql
Normal file
@@ -0,0 +1,5 @@
|
||||
mutation {
|
||||
UpdateUser {
|
||||
unreadNotificationCount
|
||||
}
|
||||
}
|
||||
32
fastanime/assets/graphql/anilist/mutations/media-list.gql
Normal file
32
fastanime/assets/graphql/anilist/mutations/media-list.gql
Normal file
@@ -0,0 +1,32 @@
|
||||
mutation (
|
||||
$mediaId: Int
|
||||
$scoreRaw: Int
|
||||
$repeat: Int
|
||||
$progress: Int
|
||||
$status: MediaListStatus
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
mediaId: $mediaId
|
||||
scoreRaw: $scoreRaw
|
||||
progress: $progress
|
||||
repeat: $repeat
|
||||
status: $status
|
||||
) {
|
||||
id
|
||||
status
|
||||
mediaId
|
||||
score
|
||||
progress
|
||||
repeat
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
}
|
||||
11
fastanime/assets/graphql/anilist/queries/logged-in-user.gql
Normal file
11
fastanime/assets/graphql/anilist/queries/logged-in-user.gql
Normal file
@@ -0,0 +1,11 @@
|
||||
query {
|
||||
Viewer {
|
||||
id
|
||||
name
|
||||
bannerImage
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page {
|
||||
media(id: $id, sort: POPULARITY_DESC, type: $type) {
|
||||
airingSchedule(notYetAired: true) {
|
||||
nodes {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
query ($id: Int, $type: MediaType) {
|
||||
Page {
|
||||
media(id: $id, type: $type) {
|
||||
characters {
|
||||
nodes {
|
||||
name {
|
||||
first
|
||||
middle
|
||||
last
|
||||
full
|
||||
native
|
||||
}
|
||||
image {
|
||||
medium
|
||||
large
|
||||
}
|
||||
description
|
||||
gender
|
||||
dateOfBirth {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
age
|
||||
bloodType
|
||||
favourites
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
query ($mediaId: Int) {
|
||||
MediaList(mediaId: $mediaId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
94
fastanime/assets/graphql/anilist/queries/media-list.gql
Normal file
94
fastanime/assets/graphql/anilist/queries/media-list.gql
Normal file
@@ -0,0 +1,94 @@
|
||||
query (
|
||||
$userId: Int
|
||||
$status: MediaListStatus
|
||||
$type: MediaType
|
||||
$page: Int
|
||||
$perPage: Int
|
||||
$sort: [MediaListSort]
|
||||
) {
|
||||
Page(perPage: $perPage, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
mediaList(userId: $userId, status: $status, type: $type, sort: $sort) {
|
||||
mediaId
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
format
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
averageScore
|
||||
episodes
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
favourites
|
||||
isAnimationStudio
|
||||
}
|
||||
}
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
description
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
status
|
||||
progress
|
||||
score
|
||||
repeat
|
||||
notes
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
query ($id: Int, $page: Int, $per_page: Int) {
|
||||
Page(perPage: $per_page, page: $page) {
|
||||
recommendations(mediaRecommendationId: $id) {
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
format
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
description
|
||||
episodes
|
||||
duration
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
fastanime/assets/graphql/anilist/queries/media-relations.gql
Normal file
61
fastanime/assets/graphql/anilist/queries/media-relations.gql
Normal file
@@ -0,0 +1,61 @@
|
||||
query ($id: Int, $format_not_in: [MediaFormat]) {
|
||||
Media(id: $id, format_not_in: $format_not_in) {
|
||||
relations {
|
||||
nodes {
|
||||
id
|
||||
idMal
|
||||
type
|
||||
format
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
description
|
||||
episodes
|
||||
duration
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
genres
|
||||
synonyms
|
||||
averageScore
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
fastanime/assets/graphql/anilist/queries/notifications.gql
Normal file
28
fastanime/assets/graphql/anilist/queries/notifications.gql
Normal file
@@ -0,0 +1,28 @@
|
||||
query {
|
||||
Page(perPage: 5) {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
notifications(resetNotificationCount: true, type: AIRING) {
|
||||
... on AiringNotification {
|
||||
id
|
||||
type
|
||||
episode
|
||||
contexts
|
||||
createdAt
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
fastanime/assets/graphql/anilist/queries/reviews.gql
Normal file
18
fastanime/assets/graphql/anilist/queries/reviews.gql
Normal file
@@ -0,0 +1,18 @@
|
||||
query ($id: Int) {
|
||||
Page {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
reviews(mediaId: $id) {
|
||||
summary
|
||||
user {
|
||||
name
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
}
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
121
fastanime/assets/graphql/anilist/queries/search.gql
Normal file
121
fastanime/assets/graphql/anilist/queries/search.gql
Normal file
@@ -0,0 +1,121 @@
|
||||
query (
|
||||
$query: String
|
||||
$per_page: Int
|
||||
$page: Int
|
||||
$sort: [MediaSort]
|
||||
$id_in: [Int]
|
||||
$genre_in: [String]
|
||||
$genre_not_in: [String]
|
||||
$tag_in: [String]
|
||||
$tag_not_in: [String]
|
||||
$status_in: [MediaStatus]
|
||||
$status: MediaStatus
|
||||
$status_not_in: [MediaStatus]
|
||||
$popularity_greater: Int
|
||||
$popularity_lesser: Int
|
||||
$averageScore_greater: Int
|
||||
$averageScore_lesser: Int
|
||||
$seasonYear: Int
|
||||
$startDate_greater: FuzzyDateInt
|
||||
$startDate_lesser: FuzzyDateInt
|
||||
$startDate: FuzzyDateInt
|
||||
$endDate_greater: FuzzyDateInt
|
||||
$endDate_lesser: FuzzyDateInt
|
||||
$format_in: [MediaFormat]
|
||||
$type: MediaType
|
||||
$season: MediaSeason
|
||||
$on_list: Boolean
|
||||
) {
|
||||
Page(perPage: $per_page, page: $page) {
|
||||
pageInfo {
|
||||
total
|
||||
currentPage
|
||||
hasNextPage
|
||||
}
|
||||
media(
|
||||
search: $query
|
||||
id_in: $id_in
|
||||
genre_in: $genre_in
|
||||
genre_not_in: $genre_not_in
|
||||
tag_in: $tag_in
|
||||
tag_not_in: $tag_not_in
|
||||
status_in: $status_in
|
||||
status: $status
|
||||
startDate: $startDate
|
||||
status_not_in: $status_not_in
|
||||
popularity_greater: $popularity_greater
|
||||
popularity_lesser: $popularity_lesser
|
||||
averageScore_greater: $averageScore_greater
|
||||
averageScore_lesser: $averageScore_lesser
|
||||
startDate_greater: $startDate_greater
|
||||
startDate_lesser: $startDate_lesser
|
||||
endDate_greater: $endDate_greater
|
||||
endDate_lesser: $endDate_lesser
|
||||
format_in: $format_in
|
||||
sort: $sort
|
||||
season: $season
|
||||
seasonYear: $seasonYear
|
||||
type: $type
|
||||
onList: $on_list
|
||||
) {
|
||||
id
|
||||
idMal
|
||||
format
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
coverImage {
|
||||
medium
|
||||
large
|
||||
}
|
||||
trailer {
|
||||
site
|
||||
id
|
||||
}
|
||||
mediaListEntry {
|
||||
status
|
||||
id
|
||||
progress
|
||||
}
|
||||
popularity
|
||||
streamingEpisodes {
|
||||
title
|
||||
thumbnail
|
||||
}
|
||||
favourites
|
||||
averageScore
|
||||
duration
|
||||
episodes
|
||||
genres
|
||||
synonyms
|
||||
studios {
|
||||
nodes {
|
||||
name
|
||||
favourites
|
||||
isAnimationStudio
|
||||
}
|
||||
}
|
||||
tags {
|
||||
name
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
status
|
||||
description
|
||||
nextAiringEpisode {
|
||||
timeUntilAiring
|
||||
airingAt
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
fastanime/assets/graphql/anilist/queries/user-info.gql
Normal file
62
fastanime/assets/graphql/anilist/queries/user-info.gql
Normal file
@@ -0,0 +1,62 @@
|
||||
query ($userId: Int) {
|
||||
User(id: $userId) {
|
||||
name
|
||||
about
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
bannerImage
|
||||
statistics {
|
||||
anime {
|
||||
count
|
||||
minutesWatched
|
||||
episodesWatched
|
||||
genres {
|
||||
count
|
||||
meanScore
|
||||
genre
|
||||
}
|
||||
tags {
|
||||
tag {
|
||||
id
|
||||
}
|
||||
count
|
||||
meanScore
|
||||
}
|
||||
}
|
||||
manga {
|
||||
count
|
||||
meanScore
|
||||
chaptersRead
|
||||
volumesRead
|
||||
tags {
|
||||
count
|
||||
meanScore
|
||||
}
|
||||
genres {
|
||||
count
|
||||
meanScore
|
||||
}
|
||||
}
|
||||
}
|
||||
favourites {
|
||||
anime {
|
||||
nodes {
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
}
|
||||
}
|
||||
manga {
|
||||
nodes {
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 197 KiB |
17
fastanime/assets/normalizer.json
Normal file
17
fastanime/assets/normalizer.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"allanime": {
|
||||
"1P": "one piece",
|
||||
"Magia Record: Mahou Shoujo Madoka☆Magica Gaiden (TV)": "Mahou Shoujo Madoka☆Magica",
|
||||
"Dungeon ni Deai o Motomeru no wa Machigatte Iru Darouka": "Dungeon ni Deai wo Motomeru no wa Machigatteiru Darou ka",
|
||||
"Hazurewaku no \"Joutai Ijou Skill\" de Saikyou ni Natta Ore ga Subete wo Juurin suru made": "Hazure Waku no [Joutai Ijou Skill] de Saikyou ni Natta Ore ga Subete wo Juurin Suru made",
|
||||
"Re:Zero kara Hajimeru Isekai Seikatsu Season 3": "Re:Zero kara Hajimeru Isekai Seikatsu 3rd Season"
|
||||
},
|
||||
"hianime": {
|
||||
"My Star": "Oshi no Ko"
|
||||
},
|
||||
"animepahe": {
|
||||
"Azumanga Daiou The Animation": "Azumanga Daioh",
|
||||
"Mairimashita! Iruma-kun 2nd Season": "Mairimashita! Iruma-kun 2",
|
||||
"Mairimashita! Iruma-kun 3rd Season": "Mairimashita! Iruma-kun 3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FastAnime Airing Schedule Info Script Template
|
||||
# This script formats and displays airing schedule details in the FZF preview pane.
|
||||
# Python injects the actual data values into the placeholders.
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Anime Title" "{ANIME_TITLE}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Total Episodes" "{TOTAL_EPISODES}"
|
||||
print_kv "Upcoming Episodes" "{UPCOMING_EPISODES}"
|
||||
|
||||
draw_rule
|
||||
|
||||
echo "{C_KEY}Next Episodes:{RESET}"
|
||||
echo
|
||||
echo "{SCHEDULE_TABLE}" | fold -s -w "$WIDTH"
|
||||
|
||||
draw_rule
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FZF Airing Schedule Preview Script Template
|
||||
#
|
||||
# This script is a template. The placeholders in curly braces, like {NAME}
|
||||
# are dynamically filled by python using .replace()
|
||||
|
||||
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
|
||||
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||
|
||||
generate_sha256() {
|
||||
local input
|
||||
|
||||
# Check if input is passed as an argument or piped
|
||||
if [ -n "$1" ]; then
|
||||
input="$1"
|
||||
else
|
||||
input=$(cat)
|
||||
fi
|
||||
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local key_len=${#key}
|
||||
local value_len=${#value}
|
||||
local multiplier="${3:-1}"
|
||||
|
||||
# Correctly calculate padding by accounting for the key, the ": ", and the value.
|
||||
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
|
||||
|
||||
# If the text is too long to fit, just add a single space for separation.
|
||||
if [ "$padding_len" -lt 1 ]; then
|
||||
padding_len=1
|
||||
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
else
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
draw_rule(){
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{C_RULE}─{RESET}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
title={}
|
||||
hash=$(generate_sha256 "$title")
|
||||
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
|
||||
if [ -f "$info_file" ]; then
|
||||
source "$info_file"
|
||||
else
|
||||
echo "📅 Loading airing schedule..."
|
||||
fi
|
||||
fi
|
||||
41
fastanime/assets/scripts/fzf/character-info.template.sh
Normal file
41
fastanime/assets/scripts/fzf/character-info.template.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FastAnime Character Info Script Template
|
||||
# This script formats and displays character details in the FZF preview pane.
|
||||
# Python injects the actual data values into the placeholders.
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Character Name" "{CHARACTER_NAME}"
|
||||
|
||||
if [ -n "{CHARACTER_NATIVE_NAME}" ] && [ "{CHARACTER_NATIVE_NAME}" != "N/A" ]; then
|
||||
print_kv "Native Name" "{CHARACTER_NATIVE_NAME}"
|
||||
fi
|
||||
|
||||
draw_rule
|
||||
|
||||
if [ -n "{CHARACTER_GENDER}" ] && [ "{CHARACTER_GENDER}" != "Unknown" ]; then
|
||||
print_kv "Gender" "{CHARACTER_GENDER}"
|
||||
fi
|
||||
|
||||
if [ -n "{CHARACTER_AGE}" ] && [ "{CHARACTER_AGE}" != "Unknown" ]; then
|
||||
print_kv "Age" "{CHARACTER_AGE}"
|
||||
fi
|
||||
|
||||
if [ -n "{CHARACTER_BLOOD_TYPE}" ] && [ "{CHARACTER_BLOOD_TYPE}" != "N/A" ]; then
|
||||
print_kv "Blood Type" "{CHARACTER_BLOOD_TYPE}"
|
||||
fi
|
||||
|
||||
if [ -n "{CHARACTER_BIRTHDAY}" ] && [ "{CHARACTER_BIRTHDAY}" != "N/A" ]; then
|
||||
print_kv "Birthday" "{CHARACTER_BIRTHDAY}"
|
||||
fi
|
||||
|
||||
if [ -n "{CHARACTER_FAVOURITES}" ] && [ "{CHARACTER_FAVOURITES}" != "0" ]; then
|
||||
print_kv "Favorites" "{CHARACTER_FAVOURITES}"
|
||||
fi
|
||||
|
||||
draw_rule
|
||||
|
||||
echo "{CHARACTER_DESCRIPTION}" | fold -s -w "$WIDTH"
|
||||
|
||||
draw_rule
|
||||
130
fastanime/assets/scripts/fzf/character-preview.template.sh
Normal file
130
fastanime/assets/scripts/fzf/character-preview.template.sh
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FZF Character Preview Script Template
|
||||
#
|
||||
# This script is a template. The placeholders in curly braces, like {NAME}
|
||||
# are dynamically filled by python using .replace()
|
||||
|
||||
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
|
||||
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||
|
||||
generate_sha256() {
|
||||
local input
|
||||
|
||||
# Check if input is passed as an argument or piped
|
||||
if [ -n "$1" ]; then
|
||||
input="$1"
|
||||
else
|
||||
input=$(cat)
|
||||
fi
|
||||
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
fzf_preview() {
|
||||
file=$1
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ "$dim" = x ]; then
|
||||
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
|
||||
fi
|
||||
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
fi
|
||||
|
||||
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
chafa -s "$dim" "$file"
|
||||
fi
|
||||
elif command -v chafa >/dev/null 2>&1; then
|
||||
case "$PLATFORM" in
|
||||
android) chafa -s "$dim" "$file" ;;
|
||||
windows) chafa -f sixel -s "$dim" "$file" ;;
|
||||
*) chafa -s "$dim" "$file" ;;
|
||||
esac
|
||||
echo
|
||||
|
||||
elif command -v imgcat >/dev/null; then
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
else
|
||||
echo please install a terminal image viewer
|
||||
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
||||
fi
|
||||
}
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local key_len=${#key}
|
||||
local value_len=${#value}
|
||||
local multiplier="${3:-1}"
|
||||
|
||||
# Correctly calculate padding by accounting for the key, the ": ", and the value.
|
||||
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
|
||||
|
||||
# If the text is too long to fit, just add a single space for separation.
|
||||
if [ "$padding_len" -lt 1 ]; then
|
||||
padding_len=1
|
||||
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
else
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
draw_rule(){
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{C_RULE}─{RESET}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
title={}
|
||||
hash=$(generate_sha256 "$title")
|
||||
|
||||
|
||||
# FIXME: Disabled since they cover the text perhaps its aspect ratio related or image format not sure
|
||||
# if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
|
||||
# image_file="{IMAGE_CACHE_DIR}{PATH_SEP}$hash.png"
|
||||
# if [ -f "$image_file" ]; then
|
||||
# fzf_preview "$image_file"
|
||||
# echo # Add a newline for spacing
|
||||
# fi
|
||||
# fi
|
||||
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
|
||||
if [ -f "$info_file" ]; then
|
||||
source "$info_file"
|
||||
else
|
||||
echo "👤 Loading character details..."
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
315
fastanime/assets/scripts/fzf/dynamic-preview.template.sh
Normal file
315
fastanime/assets/scripts/fzf/dynamic-preview.template.sh
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# FZF Dynamic Preview Script Template
|
||||
#
|
||||
# This script handles previews for dynamic search results by parsing the JSON
|
||||
# search results file and extracting info for the selected item.
|
||||
# The placeholders in curly braces are dynamically filled by Python using .replace()
|
||||
|
||||
WIDTH=${FZF_PREVIEW_COLUMNS:-80}
|
||||
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||
SEARCH_RESULTS_FILE="{SEARCH_RESULTS_FILE}"
|
||||
IMAGE_CACHE_PATH="{IMAGE_CACHE_PATH}"
|
||||
INFO_CACHE_PATH="{INFO_CACHE_PATH}"
|
||||
PATH_SEP="{PATH_SEP}"
|
||||
|
||||
# Color codes injected by Python
|
||||
C_TITLE="{C_TITLE}"
|
||||
C_KEY="{C_KEY}"
|
||||
C_VALUE="{C_VALUE}"
|
||||
C_RULE="{C_RULE}"
|
||||
RESET="{RESET}"
|
||||
|
||||
# Selected item from fzf
|
||||
SELECTED_ITEM={}
|
||||
|
||||
generate_sha256() {
|
||||
local input="$1"
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
fzf_preview() {
|
||||
file=$1
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ "$dim" = x ]; then
|
||||
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
|
||||
fi
|
||||
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
fi
|
||||
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
chafa -s "$dim" "$file"
|
||||
fi
|
||||
elif command -v chafa >/dev/null 2>&1; then
|
||||
case "$PLATFORM" in
|
||||
android) chafa -s "$dim" "$file" ;;
|
||||
windows) chafa -f sixel -s "$dim" "$file" ;;
|
||||
*) chafa -s "$dim" "$file" ;;
|
||||
esac
|
||||
echo
|
||||
elif command -v imgcat >/dev/null; then
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
else
|
||||
echo please install a terminal image viewer
|
||||
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
||||
fi
|
||||
}
|
||||
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local key_len=${#key}
|
||||
local value_len=${#value}
|
||||
local multiplier="${3:-1}"
|
||||
|
||||
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
|
||||
|
||||
if [ "$padding_len" -lt 1 ]; then
|
||||
padding_len=1
|
||||
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
else
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
fi
|
||||
}
|
||||
|
||||
draw_rule() {
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{C_RULE}─{RESET}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
clean_html() {
|
||||
echo "$1" | sed 's/<[^>]*>//g' | sed 's/</</g' | sed 's/>/>/g' | sed 's/&/\&/g' | sed 's/"/"/g' | sed "s/'/'/g"
|
||||
}
|
||||
|
||||
format_date() {
|
||||
local date_obj="$1"
|
||||
if [ "$date_obj" = "null" ] || [ -z "$date_obj" ]; then
|
||||
echo "N/A"
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract year, month, day from the date object
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
year=$(echo "$date_obj" | jq -r '.year // "N/A"' 2>/dev/null || echo "N/A")
|
||||
month=$(echo "$date_obj" | jq -r '.month // ""' 2>/dev/null || echo "")
|
||||
day=$(echo "$date_obj" | jq -r '.day // ""' 2>/dev/null || echo "")
|
||||
else
|
||||
year=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('year', 'N/A'))" 2>/dev/null || echo "N/A")
|
||||
month=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('month', ''))" 2>/dev/null || echo "")
|
||||
day=$(echo "$date_obj" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('day', ''))" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ "$year" = "N/A" ] || [ "$year" = "null" ]; then
|
||||
echo "N/A"
|
||||
elif [ -n "$month" ] && [ "$month" != "null" ] && [ -n "$day" ] && [ "$day" != "null" ]; then
|
||||
echo "$day/$month/$year"
|
||||
elif [ -n "$month" ] && [ "$month" != "null" ]; then
|
||||
echo "$month/$year"
|
||||
else
|
||||
echo "$year"
|
||||
fi
|
||||
}
|
||||
|
||||
# If no selection or search results file doesn't exist, show placeholder
|
||||
if [ -z "$SELECTED_ITEM" ] || [ ! -f "$SEARCH_RESULTS_FILE" ]; then
|
||||
echo "${C_TITLE}Dynamic Search Preview${RESET}"
|
||||
draw_rule
|
||||
echo "Type to search for anime..."
|
||||
echo "Results will appear here as you type."
|
||||
echo
|
||||
echo "DEBUG:"
|
||||
echo "SELECTED_ITEM='$SELECTED_ITEM'"
|
||||
echo "SEARCH_RESULTS_FILE='$SEARCH_RESULTS_FILE'"
|
||||
if [ -f "$SEARCH_RESULTS_FILE" ]; then
|
||||
echo "Search results file exists"
|
||||
else
|
||||
echo "Search results file missing"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
# Parse the search results JSON and find the matching item
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
MEDIA_DATA=$(cat "$SEARCH_RESULTS_FILE" | jq --arg anime_title "$SELECTED_ITEM" '
|
||||
.data.Page.media[]? |
|
||||
select((.title.english // .title.romaji // .title.native // "Unknown") == $anime_title )
|
||||
' )
|
||||
else
|
||||
# Fallback to Python for JSON parsing
|
||||
MEDIA_DATA=$(cat "$SEARCH_RESULTS_FILE" | python3 -c "
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
selected_item = '''$SELECTED_ITEM'''
|
||||
|
||||
if 'data' not in data or 'Page' not in data['data'] or 'media' not in data['data']['Page']:
|
||||
sys.exit(1)
|
||||
|
||||
media_list = data['data']['Page']['media']
|
||||
|
||||
for media in media_list:
|
||||
title = media.get('title', {})
|
||||
english_title = title.get('english') or title.get('romaji') or title.get('native', 'Unknown')
|
||||
year = media.get('startDate', {}).get('year', 'Unknown') if media.get('startDate') else 'Unknown'
|
||||
status = media.get('status', 'Unknown')
|
||||
genres = ', '.join(media.get('genres', [])[:3]) or 'Unknown'
|
||||
display_format = f'{english_title} ({year}) [{status}] - {genres}'
|
||||
# Debug output for matching
|
||||
print(f"DEBUG: selected_item='{selected_item.strip()}' display_format='{display_format.strip()}'", file=sys.stderr)
|
||||
if selected_item.strip() == display_format.strip():
|
||||
json.dump(media, sys.stdout, indent=2)
|
||||
sys.exit(0)
|
||||
print(f"DEBUG: No match found for selected_item='{selected_item.strip()}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f'Error: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# If we couldn't find the media data, show error
|
||||
if [ $? -ne 0 ] || [ -z "$MEDIA_DATA" ]; then
|
||||
echo "${C_TITLE}Preview Error${RESET}"
|
||||
draw_rule
|
||||
echo "Could not load preview data for:"
|
||||
echo "$SELECTED_ITEM"
|
||||
echo
|
||||
echo "DEBUG INFO:"
|
||||
echo "Search results file: $SEARCH_RESULTS_FILE"
|
||||
if [ -f "$SEARCH_RESULTS_FILE" ]; then
|
||||
echo "File exists, size: $(wc -c < "$SEARCH_RESULTS_FILE") bytes"
|
||||
echo "First few lines of search results:"
|
||||
head -3 "$SEARCH_RESULTS_FILE" 2>/dev/null || echo "Cannot read file"
|
||||
else
|
||||
echo "Search results file does not exist"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract information from the media data
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
# Use jq for faster extraction
|
||||
TITLE=$(echo "$MEDIA_DATA" | jq -r '.title.english // .title.romaji // .title.native // "Unknown"' 2>/dev/null || echo "Unknown")
|
||||
STATUS=$(echo "$MEDIA_DATA" | jq -r '.status // "Unknown"' 2>/dev/null || echo "Unknown")
|
||||
FORMAT=$(echo "$MEDIA_DATA" | jq -r '.format // "Unknown"' 2>/dev/null || echo "Unknown")
|
||||
EPISODES=$(echo "$MEDIA_DATA" | jq -r '.episodes // "Unknown"' 2>/dev/null || echo "Unknown")
|
||||
DURATION=$(echo "$MEDIA_DATA" | jq -r 'if .duration then "\(.duration) min" else "Unknown" end' 2>/dev/null || echo "Unknown")
|
||||
SCORE=$(echo "$MEDIA_DATA" | jq -r 'if .averageScore then "\(.averageScore)/100" else "N/A" end' 2>/dev/null || echo "N/A")
|
||||
FAVOURITES=$(echo "$MEDIA_DATA" | jq -r '.favourites // 0' 2>/dev/null | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta' || echo "0")
|
||||
POPULARITY=$(echo "$MEDIA_DATA" | jq -r '.popularity // 0' 2>/dev/null | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta' || echo "0")
|
||||
GENRES=$(echo "$MEDIA_DATA" | jq -r '(.genres[:5] // []) | join(", ") | if . == "" then "Unknown" else . end' 2>/dev/null || echo "Unknown")
|
||||
DESCRIPTION=$(echo "$MEDIA_DATA" | jq -r '.description // "No description available."' 2>/dev/null || echo "No description available.")
|
||||
|
||||
# Get start and end dates as JSON objects
|
||||
START_DATE_OBJ=$(echo "$MEDIA_DATA" | jq -c '.startDate' 2>/dev/null || echo "null")
|
||||
END_DATE_OBJ=$(echo "$MEDIA_DATA" | jq -c '.endDate' 2>/dev/null || echo "null")
|
||||
|
||||
# Get cover image URL
|
||||
COVER_IMAGE=$(echo "$MEDIA_DATA" | jq -r '.coverImage.large // ""' 2>/dev/null || echo "")
|
||||
else
|
||||
# Fallback to Python for extraction
|
||||
TITLE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); title=data.get('title',{}); print(title.get('english') or title.get('romaji') or title.get('native', 'Unknown'))" 2>/dev/null || echo "Unknown")
|
||||
STATUS=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('status', 'Unknown'))" 2>/dev/null || echo "Unknown")
|
||||
FORMAT=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('format', 'Unknown'))" 2>/dev/null || echo "Unknown")
|
||||
EPISODES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('episodes', 'Unknown'))" 2>/dev/null || echo "Unknown")
|
||||
DURATION=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); duration=data.get('duration'); print(f'{duration} min' if duration else 'Unknown')" 2>/dev/null || echo "Unknown")
|
||||
SCORE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); score=data.get('averageScore'); print(f'{score}/100' if score else 'N/A')" 2>/dev/null || echo "N/A")
|
||||
FAVOURITES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(f\"{data.get('favourites', 0):,}\")" 2>/dev/null || echo "0")
|
||||
POPULARITY=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(f\"{data.get('popularity', 0):,}\")" 2>/dev/null || echo "0")
|
||||
GENRES=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(', '.join(data.get('genres', [])[:5]))" 2>/dev/null || echo "Unknown")
|
||||
DESCRIPTION=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); print(data.get('description', 'No description available.'))" 2>/dev/null || echo "No description available.")
|
||||
|
||||
# Get start and end dates
|
||||
START_DATE_OBJ=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); json.dump(data.get('startDate'), sys.stdout)" 2>/dev/null || echo "null")
|
||||
END_DATE_OBJ=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); json.dump(data.get('endDate'), sys.stdout)" 2>/dev/null || echo "null")
|
||||
|
||||
# Get cover image URL
|
||||
COVER_IMAGE=$(echo "$MEDIA_DATA" | python3 -c "import json, sys; data=json.load(sys.stdin); cover=data.get('coverImage',{}); print(cover.get('large', ''))" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Format the dates
|
||||
START_DATE=$(format_date "$START_DATE_OBJ")
|
||||
END_DATE=$(format_date "$END_DATE_OBJ")
|
||||
|
||||
# Generate cache hash for this item (using selected item like regular preview)
|
||||
CACHE_HASH=$(generate_sha256 "$SELECTED_ITEM")
|
||||
|
||||
# Try to show image if available
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
|
||||
image_file="{IMAGE_CACHE_PATH}{PATH_SEP}${CACHE_HASH}.png"
|
||||
|
||||
# If image not cached and we have a URL, try to download it quickly
|
||||
if [ ! -f "$image_file" ] && [ -n "$COVER_IMAGE" ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
# Quick download with timeout
|
||||
curl -s -m 3 -L "$COVER_IMAGE" -o "$image_file" 2>/dev/null || rm -f "$image_file" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$image_file" ]; then
|
||||
fzf_preview "$image_file"
|
||||
else
|
||||
echo "🖼️ Loading image..."
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
|
||||
# Display text info if configured
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||
draw_rule
|
||||
print_kv "Title" "$TITLE"
|
||||
draw_rule
|
||||
|
||||
print_kv "Score" "$SCORE"
|
||||
print_kv "Favourites" "$FAVOURITES"
|
||||
print_kv "Popularity" "$POPULARITY"
|
||||
print_kv "Status" "$STATUS"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Episodes" "$EPISODES"
|
||||
print_kv "Duration" "$DURATION"
|
||||
print_kv "Format" "$FORMAT"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Genres" "$GENRES"
|
||||
print_kv "Start Date" "$START_DATE"
|
||||
print_kv "End Date" "$END_DATE"
|
||||
|
||||
draw_rule
|
||||
|
||||
# Clean and display description
|
||||
CLEAN_DESCRIPTION=$(clean_html "$DESCRIPTION")
|
||||
echo "$CLEAN_DESCRIPTION" | fold -s -w "$WIDTH"
|
||||
fi
|
||||
31
fastanime/assets/scripts/fzf/episode-info.template.sh
Executable file
31
fastanime/assets/scripts/fzf/episode-info.template.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Episode Preview Info Script Template
|
||||
# This script formats and displays episode information in the FZF preview pane.
|
||||
# Some values are injected by python those with '{name}' syntax using .replace()
|
||||
|
||||
draw_rule
|
||||
|
||||
echo "{TITLE}" | fold -s -w "$WIDTH"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Duration" "{DURATION}"
|
||||
print_kv "Status" "{STATUS}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Total Episodes" "{EPISODES}"
|
||||
print_kv "Next Episode" "{NEXT_EPISODE}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Progress" "{USER_PROGRESS}"
|
||||
print_kv "List Status" "{USER_STATUS}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Start Date" "{START_DATE}"
|
||||
print_kv "End Date" "{END_DATE}"
|
||||
|
||||
draw_rule
|
||||
54
fastanime/assets/scripts/fzf/info.template.sh
Executable file
54
fastanime/assets/scripts/fzf/info.template.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FastAnime Preview Info Script Template
|
||||
# This script formats and displays the textual information in the FZF preview pane.
|
||||
# Some values are injected by python those with '{name}' syntax using .replace()
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Title" "{TITLE}"
|
||||
|
||||
draw_rule
|
||||
|
||||
# Emojis take up double the space
|
||||
score_multiplier=1
|
||||
if ! [ "{SCORE}" = "N/A" ]; then
|
||||
score_multiplier=2
|
||||
fi
|
||||
print_kv "Score" "{SCORE}" $score_multiplier
|
||||
|
||||
print_kv "Favourites" "{FAVOURITES}"
|
||||
print_kv "Popularity" "{POPULARITY}"
|
||||
print_kv "Status" "{STATUS}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Episodes" "{EPISODES}"
|
||||
print_kv "Next Episode" "{NEXT_EPISODE}"
|
||||
print_kv "Duration" "{DURATION}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Genres" "{GENRES}"
|
||||
print_kv "Format" "{FORMAT}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "List Status" "{USER_STATUS}"
|
||||
print_kv "Progress" "{USER_PROGRESS}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Start Date" "{START_DATE}"
|
||||
print_kv "End Date" "{END_DATE}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Studios" "{STUDIOS}"
|
||||
print_kv "Synonymns" "{SYNONYMNS}"
|
||||
print_kv "Tags" "{TAGS}"
|
||||
|
||||
draw_rule
|
||||
|
||||
# Synopsis
|
||||
echo "{SYNOPSIS}" | fold -s -w "$WIDTH"
|
||||
147
fastanime/assets/scripts/fzf/preview.template.sh
Executable file
147
fastanime/assets/scripts/fzf/preview.template.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FZF Preview Script Template
|
||||
#
|
||||
# This script is a template. The placeholders in curly braces, like {NAME}
|
||||
# are dynamically filled by python using .replace()
|
||||
|
||||
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
|
||||
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||
|
||||
generate_sha256() {
|
||||
local input
|
||||
|
||||
# Check if input is passed as an argument or piped
|
||||
if [ -n "$1" ]; then
|
||||
input="$1"
|
||||
else
|
||||
input=$(cat)
|
||||
fi
|
||||
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
fzf_preview() {
|
||||
file=$1
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [ "$dim" = x ]; then
|
||||
dim=$(stty size </dev/tty | awk "{print \$2 \"x\" \$1}")
|
||||
fi
|
||||
if ! [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$KITTY_WINDOW_ID" ] && [ "$((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES))" -eq "$(stty size </dev/tty | awk "{print \$1}")" ]; then
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
if [ "$IMAGE_RENDERER" = "icat" ] && [ -z "$GHOSTTY_BIN_DIR" ]; then
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
fi
|
||||
|
||||
elif [ -n "$GHOSTTY_BIN_DIR" ]; then
|
||||
dim=$((FZF_PREVIEW_COLUMNS - 1))x${FZF_PREVIEW_LINES}
|
||||
if command -v kitten >/dev/null 2>&1; then
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
elif command -v icat >/dev/null 2>&1; then
|
||||
icat --clear --transfer-mode=memory --unicode-placeholder{SCALE_UP} --stdin=no --place="$dim@0x0" "$file" | sed "\$d" | sed "$(printf "\$s/\$/\033[m/")"
|
||||
else
|
||||
chafa -s "$dim" "$file"
|
||||
fi
|
||||
elif command -v chafa >/dev/null 2>&1; then
|
||||
case "$PLATFORM" in
|
||||
android) chafa -s "$dim" "$file" ;;
|
||||
windows) chafa -f sixel -s "$dim" "$file" ;;
|
||||
*) chafa -s "$dim" "$file" ;;
|
||||
esac
|
||||
echo
|
||||
|
||||
elif command -v imgcat >/dev/null; then
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
else
|
||||
echo please install a terminal image viewer
|
||||
echo either icat for kitty terminal and wezterm or imgcat or chafa
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# --- Helper function for printing a key-value pair, aligning the value to the right ---
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local key_len=${#key}
|
||||
local value_len=${#value}
|
||||
local multiplier="${3:-1}"
|
||||
|
||||
# Correctly calculate padding by accounting for the key, the ": ", and the value.
|
||||
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
|
||||
|
||||
# If the text is too long to fit, just add a single space for separation.
|
||||
if [ "$padding_len" -lt 1 ]; then
|
||||
padding_len=1
|
||||
value=$(echo "$value"| fold -s -w "$((WIDTH - key_len - 3))")
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
else
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Draw a rule across the screen ---
|
||||
# TODO: figure out why this method does not work in fzf
|
||||
draw_rule() {
|
||||
local rule
|
||||
# Generate the line of '─' characters, removing the trailing newline `tr` adds.
|
||||
rule=$(printf '%*s' "$WIDTH" | tr ' ' '─' | tr -d '\n')
|
||||
# Print the rule with colors and a single, clean newline.
|
||||
printf "{C_RULE}%s{RESET}\\n" "$rule"
|
||||
}
|
||||
|
||||
|
||||
draw_rule(){
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{C_RULE}─{RESET}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
# Generate the same cache key that the Python worker uses
|
||||
# {PREFIX} is used only on episode previews to make sure they are unique
|
||||
title={}
|
||||
hash=$(generate_sha256 "{PREFIX}$title")
|
||||
|
||||
#
|
||||
# --- Display image if configured and the cached file exists ---
|
||||
#
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "image" ]; then
|
||||
image_file="{IMAGE_CACHE_PATH}{PATH_SEP}$hash.png"
|
||||
if [ -f "$image_file" ]; then
|
||||
fzf_preview "$image_file"
|
||||
else
|
||||
echo "🖼️ Loading image..."
|
||||
fi
|
||||
echo # Add a newline for spacing
|
||||
fi
|
||||
# Display text info if configured and the cached file exists
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||
info_file="{INFO_CACHE_PATH}{PATH_SEP}$hash"
|
||||
if [ -f "$info_file" ]; then
|
||||
source "$info_file"
|
||||
else
|
||||
echo "📝 Loading details..."
|
||||
fi
|
||||
fi
|
||||
19
fastanime/assets/scripts/fzf/review-info.template.sh
Normal file
19
fastanime/assets/scripts/fzf/review-info.template.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FastAnime Review Info Script Template
|
||||
# This script formats and displays review details in the FZF preview pane.
|
||||
# Python injects the actual data values into the placeholders.
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Review By" "{REVIEWER_NAME}"
|
||||
|
||||
draw_rule
|
||||
|
||||
print_kv "Summary" "{REVIEW_SUMMARY}"
|
||||
|
||||
draw_rule
|
||||
|
||||
echo "{REVIEW_BODY}" | fold -s -w "$WIDTH"
|
||||
|
||||
draw_rule
|
||||
75
fastanime/assets/scripts/fzf/review-preview.template.sh
Normal file
75
fastanime/assets/scripts/fzf/review-preview.template.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# FZF Preview Script Template
|
||||
#
|
||||
# This script is a template. The placeholders in curly braces, like {NAME}
|
||||
# are dynamically filled by python using .replace()
|
||||
|
||||
WIDTH=${FZF_PREVIEW_COLUMNS:-80} # Set a fallback width of 80
|
||||
IMAGE_RENDERER="{IMAGE_RENDERER}"
|
||||
|
||||
generate_sha256() {
|
||||
local input
|
||||
|
||||
# Check if input is passed as an argument or piped
|
||||
if [ -n "$1" ]; then
|
||||
input="$1"
|
||||
else
|
||||
input=$(cat)
|
||||
fi
|
||||
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
echo -n "$input" | sha256sum | awk '{print $1}'
|
||||
elif command -v shasum &>/dev/null; then
|
||||
echo -n "$input" | shasum -a 256 | awk '{print $1}'
|
||||
elif command -v sha256 &>/dev/null; then
|
||||
echo -n "$input" | sha256 | awk '{print $1}'
|
||||
elif command -v openssl &>/dev/null; then
|
||||
echo -n "$input" | openssl dgst -sha256 | awk '{print $2}'
|
||||
else
|
||||
echo -n "$input" | base64 | tr '/+' '_-' | tr -d '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
print_kv() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local key_len=${#key}
|
||||
local value_len=${#value}
|
||||
local multiplier="${3:-1}"
|
||||
|
||||
# Correctly calculate padding by accounting for the key, the ": ", and the value.
|
||||
local padding_len=$((WIDTH - key_len - 2 - value_len * multiplier))
|
||||
|
||||
# If the text is too long to fit, just add a single space for separation.
|
||||
if [ "$padding_len" -lt 1 ]; then
|
||||
padding_len=1
|
||||
value=$(echo $value| fold -s -w "$((WIDTH - key_len - 3))")
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
else
|
||||
printf "{C_KEY}%s:{RESET}%*s%s\\n" "$key" "$padding_len" "" " $value"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
draw_rule(){
|
||||
ll=2
|
||||
while [ $ll -le $FZF_PREVIEW_COLUMNS ];do
|
||||
echo -n -e "{C_RULE}─{RESET}"
|
||||
((ll++))
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
title={}
|
||||
hash=$(generate_sha256 "$title")
|
||||
|
||||
if [ "{PREVIEW_MODE}" = "full" ] || [ "{PREVIEW_MODE}" = "text" ]; then
|
||||
info_file="{INFO_CACHE_DIR}{PATH_SEP}$hash"
|
||||
if [ -f "$info_file" ]; then
|
||||
source "$info_file"
|
||||
else
|
||||
echo "📝 Loading details..."
|
||||
fi
|
||||
fi
|
||||
118
fastanime/assets/scripts/fzf/search.template.sh
Executable file
118
fastanime/assets/scripts/fzf/search.template.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# FZF Dynamic Search Script Template
|
||||
#
|
||||
# This script is a template for dynamic search functionality in fzf.
|
||||
# The placeholders in curly braces, like {QUERY} are dynamically filled by Python using .replace()
|
||||
|
||||
# Configuration variables (injected by Python)
|
||||
GRAPHQL_ENDPOINT="{GRAPHQL_ENDPOINT}"
|
||||
CACHE_DIR="{CACHE_DIR}"
|
||||
SEARCH_RESULTS_FILE="{SEARCH_RESULTS_FILE}"
|
||||
AUTH_HEADER="{AUTH_HEADER}"
|
||||
|
||||
# Get the current query from fzf
|
||||
QUERY="{{q}}"
|
||||
|
||||
# If query is empty, exit with empty results
|
||||
if [ -z "$QUERY" ]; then
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create GraphQL variables
|
||||
VARIABLES=$(cat <<EOF
|
||||
{
|
||||
"query": "$QUERY",
|
||||
"type": "ANIME",
|
||||
"per_page": 50,
|
||||
"genre_not_in": ["Hentai"]
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# The GraphQL query is injected here as a properly escaped string
|
||||
GRAPHQL_QUERY='{GRAPHQL_QUERY}'
|
||||
|
||||
# Create the GraphQL request payload
|
||||
PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"query": $GRAPHQL_QUERY,
|
||||
"variables": $VARIABLES
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# Make the GraphQL request and save raw results
|
||||
if [ -n "$AUTH_HEADER" ]; then
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: $AUTH_HEADER" \
|
||||
-d "$PAYLOAD" \
|
||||
"$GRAPHQL_ENDPOINT")
|
||||
else
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$GRAPHQL_ENDPOINT")
|
||||
fi
|
||||
|
||||
# Check if the request was successful
|
||||
if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
|
||||
echo "❌ Search failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save the raw response for later processing
|
||||
echo "$RESPONSE" > "$SEARCH_RESULTS_FILE"
|
||||
|
||||
# Parse and display results
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
# Use jq for faster and more reliable JSON parsing
|
||||
echo "$RESPONSE" | jq -r '
|
||||
if .errors then
|
||||
"❌ Search error: " + (.errors | tostring)
|
||||
elif (.data.Page.media // []) | length == 0 then
|
||||
"❌ No results found"
|
||||
else
|
||||
.data.Page.media[] | (.title.english // .title.romaji // .title.native // "Unknown")
|
||||
end
|
||||
' 2>/dev/null || echo "❌ Parse error"
|
||||
else
|
||||
# Fallback to Python for JSON parsing
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
|
||||
if 'errors' in data:
|
||||
print('❌ Search error: ' + str(data['errors']))
|
||||
sys.exit(1)
|
||||
|
||||
if 'data' not in data or 'Page' not in data['data'] or 'media' not in data['data']['Page']:
|
||||
print('❌ No results found')
|
||||
sys.exit(0)
|
||||
|
||||
media_list = data['data']['Page']['media']
|
||||
|
||||
if not media_list:
|
||||
print('❌ No results found')
|
||||
sys.exit(0)
|
||||
|
||||
for media in media_list:
|
||||
title = media.get('title', {})
|
||||
english_title = title.get('english') or title.get('romaji') or title.get('native', 'Unknown')
|
||||
year = media.get('startDate', {}).get('year', 'Unknown') if media.get('startDate') else 'Unknown'
|
||||
status = media.get('status', 'Unknown')
|
||||
genres = ', '.join(media.get('genres', [])[:3]) or 'Unknown'
|
||||
|
||||
# Format: Title (Year) [Status] - Genres
|
||||
print(f'{english_title} ({year}) [{status}] - {genres}')
|
||||
|
||||
except Exception as e:
|
||||
print(f'❌ Parse error: {str(e)}')
|
||||
sys.exit(1)
|
||||
"
|
||||
fi
|
||||
@@ -1,278 +1,3 @@
|
||||
import signal
|
||||
from .cli import cli as run_cli
|
||||
|
||||
import click
|
||||
|
||||
from .. import __version__
|
||||
from ..libs.anime_provider import SERVERS_AVAILABLE, anime_sources
|
||||
from ..Utility.data import anilist_sort_normalizer
|
||||
from .commands import LazyGroup
|
||||
|
||||
commands = {
|
||||
"search": "search.search",
|
||||
"download": "download.download",
|
||||
"anilist": "anilist.anilist",
|
||||
"config": "config.config",
|
||||
"downloads": "downloads.downloads",
|
||||
"cache": "cache.cache",
|
||||
"completions": "completions.completions",
|
||||
"update": "update.update",
|
||||
}
|
||||
|
||||
|
||||
# handle keyboard interupt
|
||||
def handle_exit(signum, frame):
|
||||
from click import clear
|
||||
|
||||
from .utils.tools import exit_app
|
||||
|
||||
clear()
|
||||
|
||||
exit_app()
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, handle_exit)
|
||||
|
||||
|
||||
@click.group(
|
||||
lazy_subcommands=commands,
|
||||
cls=LazyGroup,
|
||||
help="A command line application for streaming anime that provides a complete and featureful interface",
|
||||
short_help="Stream Anime",
|
||||
)
|
||||
@click.version_option(__version__, "--version")
|
||||
@click.option("--log", help="Allow logging to stdout", is_flag=True)
|
||||
@click.option("--log-file", help="Allow logging to a file", is_flag=True)
|
||||
@click.option("--rich-traceback", help="Use rich to output tracebacks", is_flag=True)
|
||||
@click.option(
|
||||
"-p",
|
||||
"--provider",
|
||||
type=click.Choice(list(anime_sources.keys()), case_sensitive=False),
|
||||
help="Provider of your choice",
|
||||
)
|
||||
@click.option(
|
||||
"-s",
|
||||
"--server",
|
||||
type=click.Choice([*SERVERS_AVAILABLE, "top"]),
|
||||
help="Server of choice",
|
||||
)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--format",
|
||||
type=str,
|
||||
help="yt-dlp format to use",
|
||||
)
|
||||
@click.option(
|
||||
"-c/-no-c",
|
||||
"--continue/--no-continue",
|
||||
"continue_",
|
||||
type=bool,
|
||||
help="Continue from last episode?",
|
||||
)
|
||||
@click.option(
|
||||
"--local-history/--remote-history",
|
||||
type=bool,
|
||||
help="Whether to continue from local history or remote history",
|
||||
)
|
||||
@click.option(
|
||||
"--skip/--no-skip",
|
||||
type=bool,
|
||||
help="Skip opening and ending theme songs?",
|
||||
)
|
||||
@click.option(
|
||||
"-q",
|
||||
"--quality",
|
||||
type=click.Choice(
|
||||
[
|
||||
"360",
|
||||
"480",
|
||||
"720",
|
||||
"1080",
|
||||
]
|
||||
),
|
||||
help="set the quality of the stream",
|
||||
)
|
||||
@click.option(
|
||||
"-t",
|
||||
"--translation-type",
|
||||
type=click.Choice(["dub", "sub"]),
|
||||
help="Anime language[dub/sub]",
|
||||
)
|
||||
@click.option(
|
||||
"-A/-no-A",
|
||||
"--auto-next/--no-auto-next",
|
||||
type=bool,
|
||||
help="Auto select next episode?",
|
||||
)
|
||||
@click.option(
|
||||
"-a/-no-a",
|
||||
"--auto-select/--no-auto-select",
|
||||
type=bool,
|
||||
help="Auto select anime title?",
|
||||
)
|
||||
@click.option(
|
||||
"-S",
|
||||
"--sort-by",
|
||||
type=click.Choice(anilist_sort_normalizer.keys()), # pyright: ignore
|
||||
)
|
||||
@click.option("-d", "--downloads-dir", type=click.Path(), help="Downloads location")
|
||||
@click.option("--fzf", is_flag=True, help="Use fzf for the ui")
|
||||
@click.option("--default", is_flag=True, help="Use the default interface")
|
||||
@click.option("--preview", is_flag=True, help="Show preview when using fzf")
|
||||
@click.option("--no-preview", is_flag=True, help="Dont show preview when using fzf")
|
||||
@click.option(
|
||||
"--icons/--no-icons",
|
||||
type=bool,
|
||||
help="Use icons in the interfaces",
|
||||
)
|
||||
@click.option("--dub", help="Set the translation type to dub", is_flag=True)
|
||||
@click.option("--sub", help="Set the translation type to sub", is_flag=True)
|
||||
@click.option("--rofi", help="Use rofi for the ui", is_flag=True)
|
||||
@click.option("--rofi-theme", help="Rofi theme to use", type=click.Path())
|
||||
@click.option(
|
||||
"--rofi-theme-confirm",
|
||||
help="Rofi theme to use for the confirm prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--rofi-theme-input",
|
||||
help="Rofi theme to use for the user input prompt",
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--use-mpv-mod/--use-default-player", help="Whether to use python-mpv", type=bool
|
||||
)
|
||||
@click.option("--sync-play", "-sp", help="Use sync play", is_flag=True)
|
||||
@click.pass_context
|
||||
def run_cli(
|
||||
ctx: click.Context,
|
||||
log,
|
||||
log_file,
|
||||
rich_traceback,
|
||||
provider,
|
||||
server,
|
||||
format,
|
||||
continue_,
|
||||
local_history,
|
||||
skip,
|
||||
translation_type,
|
||||
quality,
|
||||
auto_next,
|
||||
auto_select,
|
||||
sort_by,
|
||||
downloads_dir,
|
||||
fzf,
|
||||
default,
|
||||
preview,
|
||||
no_preview,
|
||||
icons,
|
||||
dub,
|
||||
sub,
|
||||
rofi,
|
||||
rofi_theme,
|
||||
rofi_theme_confirm,
|
||||
rofi_theme_input,
|
||||
use_mpv_mod,
|
||||
sync_play,
|
||||
):
|
||||
from .config import Config
|
||||
|
||||
ctx.obj = Config()
|
||||
if log:
|
||||
import logging
|
||||
|
||||
from rich.logging import RichHandler
|
||||
|
||||
FORMAT = "%(message)s"
|
||||
|
||||
logging.basicConfig(
|
||||
level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("logging has been initialized")
|
||||
elif log_file:
|
||||
import logging
|
||||
|
||||
from ..constants import NOTIFIER_LOG_FILE_PATH
|
||||
|
||||
format = "%(asctime)s%(levelname)s: %(message)s"
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
filename=NOTIFIER_LOG_FILE_PATH,
|
||||
format=format,
|
||||
datefmt="[%d/%m/%Y@%H:%M:%S]",
|
||||
filemode="w",
|
||||
)
|
||||
if rich_traceback:
|
||||
from rich.traceback import install
|
||||
|
||||
install()
|
||||
|
||||
if sync_play:
|
||||
ctx.obj.sync_play = sync_play
|
||||
if provider:
|
||||
ctx.obj.provider = provider
|
||||
if server:
|
||||
ctx.obj.server = server
|
||||
if format:
|
||||
ctx.obj.format = format
|
||||
if ctx.get_parameter_source("continue_") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.continue_from_history = continue_
|
||||
if ctx.get_parameter_source("skip") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.skip = skip
|
||||
|
||||
if quality:
|
||||
ctx.obj.quality = quality
|
||||
if ctx.get_parameter_source("auto_next") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.auto_next = auto_next
|
||||
if ctx.get_parameter_source("icons") == click.core.ParameterSource.COMMANDLINE:
|
||||
ctx.obj.icons = icons
|
||||
if (
|
||||
ctx.get_parameter_source("local_history")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.preferred_history = "local" if local_history else "remote"
|
||||
if (
|
||||
ctx.get_parameter_source("auto_select")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.auto_select = auto_select
|
||||
if (
|
||||
ctx.get_parameter_source("use_mpv_mod")
|
||||
== click.core.ParameterSource.COMMANDLINE
|
||||
):
|
||||
ctx.obj.use_mpv_mod = use_mpv_mod
|
||||
if sort_by:
|
||||
ctx.obj.sort_by = sort_by
|
||||
if downloads_dir:
|
||||
ctx.obj.downloads_dir = downloads_dir
|
||||
if translation_type:
|
||||
ctx.obj.translation_type = translation_type
|
||||
if fzf:
|
||||
ctx.obj.use_fzf = True
|
||||
if default:
|
||||
ctx.obj.use_fzf = False
|
||||
if preview:
|
||||
ctx.obj.preview = True
|
||||
if no_preview:
|
||||
ctx.obj.preview = False
|
||||
if dub:
|
||||
ctx.obj.translation_type = "dub"
|
||||
if sub:
|
||||
ctx.obj.translation_type = "sub"
|
||||
if rofi:
|
||||
ctx.obj.use_fzf = False
|
||||
ctx.obj.use_rofi = True
|
||||
if rofi:
|
||||
from ..libs.rofi import Rofi
|
||||
|
||||
if rofi_theme:
|
||||
ctx.obj.rofi_theme = rofi_theme
|
||||
Rofi.rofi_theme = rofi_theme
|
||||
|
||||
if rofi_theme_input:
|
||||
ctx.obj.rofi_theme_input = rofi_theme_input
|
||||
Rofi.rofi_theme_input = rofi_theme_input
|
||||
|
||||
if rofi_theme_confirm:
|
||||
ctx.obj.rofi_theme_confirm = rofi_theme_confirm
|
||||
Rofi.rofi_theme_confirm = rofi_theme_confirm
|
||||
__all__ = ["run_cli"]
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import pathlib
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from rich import print
|
||||
|
||||
from .. import APP_NAME, AUTHOR, GIT_REPO, __version__
|
||||
|
||||
API_URL = f"https://api.{GIT_REPO}/repos/{AUTHOR}/{APP_NAME}/releases/latest"
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
USER_AGENT = f"{APP_NAME} user"
|
||||
request = requests.get(
|
||||
API_URL,
|
||||
headers={
|
||||
"User-Agent": USER_AGENT,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Accept": "application/vnd.github+json",
|
||||
},
|
||||
)
|
||||
|
||||
if request.status_code == 200:
|
||||
release_json = request.json()
|
||||
return (release_json["tag_name"] == __version__, release_json)
|
||||
else:
|
||||
print(request.text)
|
||||
return (False, {})
|
||||
|
||||
|
||||
def is_git_repo(author, repository):
|
||||
# Check if the current directory contains a .git folder
|
||||
git_dir = pathlib.Path(".git")
|
||||
if not git_dir.exists() or not git_dir.is_dir():
|
||||
return False
|
||||
|
||||
# Check if the config file exists
|
||||
config_path = git_dir / "config"
|
||||
if not config_path.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
# Read the .git/config file to find the remote repository URL
|
||||
with config_path.open("r") as git_config:
|
||||
git_config_content = git_config.read()
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return False
|
||||
|
||||
# Use regex to find the repository URL in the config file
|
||||
repo_name_pattern = r"url\s*=\s*.+/([^/]+/[^/]+)\.git"
|
||||
match = re.search(repo_name_pattern, git_config_content)
|
||||
|
||||
# Return True if match found and repository name matches
|
||||
return bool(match) and match.group(1) == f"{author}/{repository}"
|
||||
|
||||
|
||||
def update_app():
|
||||
is_latest, release_json = check_for_updates()
|
||||
if is_latest:
|
||||
print("[green]App is up to date[/]")
|
||||
return False, release_json
|
||||
tag_name = release_json["tag_name"]
|
||||
|
||||
print("[cyan]Updating app to version %s[/]" % tag_name)
|
||||
if is_git_repo(AUTHOR, APP_NAME):
|
||||
GIT_EXECUTABLE = shutil.which("git")
|
||||
args = [
|
||||
GIT_EXECUTABLE,
|
||||
"pull",
|
||||
]
|
||||
|
||||
print(f"Pulling latest changes from the repository via git: {shlex.join(args)}")
|
||||
|
||||
if not GIT_EXECUTABLE:
|
||||
print("[red]Cannot find git please install it.[/]")
|
||||
return False, release_json
|
||||
|
||||
process = subprocess.run(
|
||||
args,
|
||||
)
|
||||
|
||||
else:
|
||||
if PIPX_EXECUTABLE := shutil.which("pipx"):
|
||||
process = subprocess.run([PIPX_EXECUTABLE, "upgrade", APP_NAME])
|
||||
else:
|
||||
PYTHON_EXECUTABLE = sys.executable
|
||||
|
||||
args = [
|
||||
PYTHON_EXECUTABLE,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
APP_NAME,
|
||||
"--user",
|
||||
"--no-warn-script-location",
|
||||
]
|
||||
process = subprocess.run(args)
|
||||
if process.returncode == 0:
|
||||
return True, release_json
|
||||
else:
|
||||
return False, release_json
|
||||
108
fastanime/cli/cli.py
Normal file
108
fastanime/cli/cli.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
from click.core import ParameterSource
|
||||
|
||||
from ..core.config import AppConfig
|
||||
from ..core.constants import PROJECT_NAME, USER_CONFIG, __version__
|
||||
from .config import ConfigLoader
|
||||
from .options import options_from_model
|
||||
from .utils.exception import setup_exceptions_handler
|
||||
from .utils.lazyloader import LazyGroup
|
||||
from .utils.logging import setup_logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
from typing_extensions import Unpack
|
||||
|
||||
class Options(TypedDict):
|
||||
no_config: bool | None
|
||||
trace: bool | None
|
||||
dev: bool | None
|
||||
log: bool | None
|
||||
rich_traceback: bool | None
|
||||
rich_traceback_theme: str
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
commands = {
|
||||
"config": "config.config",
|
||||
"search": "search.search",
|
||||
"anilist": "anilist.anilist",
|
||||
"download": "download.download",
|
||||
"update": "update.update",
|
||||
"registry": "registry.registry",
|
||||
"worker": "worker.worker",
|
||||
"queue": "queue.queue",
|
||||
}
|
||||
|
||||
|
||||
@click.group(
|
||||
cls=LazyGroup,
|
||||
root="fastanime.cli.commands",
|
||||
lazy_subcommands=commands,
|
||||
context_settings=dict(auto_envvar_prefix=PROJECT_NAME),
|
||||
)
|
||||
@click.version_option(__version__, "--version")
|
||||
@click.option("--no-config", is_flag=True, help="Don't load the user config file.")
|
||||
@click.option(
|
||||
"--trace", is_flag=True, help="Controls Whether to display tracebacks or not"
|
||||
)
|
||||
@click.option("--dev", is_flag=True, help="Controls Whether the app is in dev mode")
|
||||
@click.option("--log", is_flag=True, help="Controls Whether to log")
|
||||
@click.option(
|
||||
"--rich-traceback",
|
||||
is_flag=True,
|
||||
help="Controls Whether to display a rich traceback",
|
||||
)
|
||||
@click.option(
|
||||
"--rich-traceback-theme",
|
||||
default="github-dark",
|
||||
help="Controls Whether to display a rich traceback",
|
||||
)
|
||||
@options_from_model(AppConfig)
|
||||
@click.pass_context
|
||||
def cli(ctx: click.Context, **options: "Unpack[Options]"):
|
||||
"""
|
||||
The main entry point for the FastAnime CLI.
|
||||
"""
|
||||
setup_logging(options["log"])
|
||||
setup_exceptions_handler(
|
||||
options["trace"],
|
||||
options["dev"],
|
||||
options["rich_traceback"],
|
||||
options["rich_traceback_theme"],
|
||||
)
|
||||
|
||||
logger.info(f"Current Command: {' '.join(sys.argv)}")
|
||||
cli_overrides = {}
|
||||
param_lookup = {p.name: p for p in ctx.command.params}
|
||||
|
||||
for param_name, param_value in ctx.params.items():
|
||||
source = ctx.get_parameter_source(param_name)
|
||||
if source in (ParameterSource.ENVIRONMENT, ParameterSource.COMMANDLINE):
|
||||
parameter = param_lookup.get(param_name)
|
||||
|
||||
if (
|
||||
parameter
|
||||
and hasattr(parameter, "model_name")
|
||||
and hasattr(parameter, "field_name")
|
||||
):
|
||||
model_name = getattr(parameter, "model_name")
|
||||
field_name = getattr(parameter, "field_name")
|
||||
|
||||
if model_name not in cli_overrides:
|
||||
cli_overrides[model_name] = {}
|
||||
cli_overrides[model_name][field_name] = param_value
|
||||
|
||||
loader = ConfigLoader(config_path=USER_CONFIG)
|
||||
config = (
|
||||
AppConfig.model_validate(cli_overrides)
|
||||
if options["no_config"]
|
||||
else loader.load(cli_overrides)
|
||||
)
|
||||
ctx.obj = config
|
||||
@@ -1,40 +0,0 @@
|
||||
# in lazy_group.py
|
||||
import importlib
|
||||
|
||||
import click
|
||||
|
||||
|
||||
class LazyGroup(click.Group):
|
||||
def __init__(self, *args, lazy_subcommands=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# lazy_subcommands is a map of the form:
|
||||
#
|
||||
# {command-name} -> {module-name}.{command-object-name}
|
||||
#
|
||||
self.lazy_subcommands = lazy_subcommands or {}
|
||||
|
||||
def list_commands(self, ctx):
|
||||
base = super().list_commands(ctx)
|
||||
lazy = sorted(self.lazy_subcommands.keys())
|
||||
return base + lazy
|
||||
|
||||
def get_command(self, ctx, cmd_name): # pyright:ignore
|
||||
if cmd_name in self.lazy_subcommands:
|
||||
return self._lazy_load(cmd_name)
|
||||
return super().get_command(ctx, cmd_name)
|
||||
|
||||
def _lazy_load(self, cmd_name: str):
|
||||
# lazily loading a command, first get the module name and attribute name
|
||||
import_path: str = self.lazy_subcommands[cmd_name]
|
||||
modname, cmd_object_name = import_path.rsplit(".", 1)
|
||||
# do the import
|
||||
mod = importlib.import_module(f".{modname}", package="fastanime.cli.commands")
|
||||
# get the Command object from that module
|
||||
cmd_object = getattr(mod, cmd_object_name)
|
||||
# check the result to make debugging easier
|
||||
if not isinstance(cmd_object, click.BaseCommand):
|
||||
raise ValueError(
|
||||
f"Lazy loading of {import_path} failed by returning "
|
||||
"a non-command object"
|
||||
)
|
||||
return cmd_object
|
||||
|
||||
@@ -1,51 +1,3 @@
|
||||
import click
|
||||
from .cmd import anilist
|
||||
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
from .__lazyloader__ import LazyGroup
|
||||
|
||||
commands = {
|
||||
"trending": "trending.trending",
|
||||
"recent": "recent.recent",
|
||||
"search": "search.search",
|
||||
"upcoming": "upcoming.upcoming",
|
||||
"scores": "scores.scores",
|
||||
"popular": "popular.popular",
|
||||
"favourites": "favourites.favourites",
|
||||
"random": "random_anime.random_anime",
|
||||
"login": "login.login",
|
||||
"watching": "watching.watching",
|
||||
"paused": "paused.paused",
|
||||
"rewatching": "rewatching.rewatching",
|
||||
"dropped": "dropped.dropped",
|
||||
"completed": "completed.completed",
|
||||
"planning": "planning.planning",
|
||||
"notifier": "notifier.notifier",
|
||||
}
|
||||
|
||||
|
||||
@click.group(
|
||||
lazy_subcommands=commands,
|
||||
cls=LazyGroup,
|
||||
invoke_without_command=True,
|
||||
help="A beautiful interface that gives you access to a commplete streaming experience",
|
||||
short_help="Access all streaming options",
|
||||
)
|
||||
@click.pass_context
|
||||
def anilist(ctx: click.Context):
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....AnimeProvider import AnimeProvider
|
||||
from ...interfaces.anilist_interfaces import (
|
||||
fastanime_main_menu as anilist_interface,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
config: Config = ctx.obj
|
||||
config.anime_provider = AnimeProvider(config.provider)
|
||||
if user := ctx.obj.user:
|
||||
AniList.update_login_info(user, user["token"])
|
||||
if ctx.invoked_subcommand is None:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
anilist_interface(ctx.obj, fastanime_runtime_state)
|
||||
__all__ = ["anilist"]
|
||||
|
||||
43
fastanime/cli/commands/anilist/cmd.py
Normal file
43
fastanime/cli/commands/anilist/cmd.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import click
|
||||
|
||||
from ...utils.lazyloader import LazyGroup
|
||||
from . import examples
|
||||
|
||||
commands = {
|
||||
# "trending": "trending.trending",
|
||||
# "recent": "recent.recent",
|
||||
"search": "search.search",
|
||||
"download": "download.download",
|
||||
"downloads": "downloads.downloads",
|
||||
"auth": "auth.auth",
|
||||
"stats": "stats.stats",
|
||||
"notifications": "notifications.notifications",
|
||||
}
|
||||
|
||||
|
||||
@click.group(
|
||||
cls=LazyGroup,
|
||||
name="anilist",
|
||||
root="fastanime.cli.commands.anilist.commands",
|
||||
invoke_without_command=True,
|
||||
help="A beautiful interface that gives you access to a commplete streaming experience",
|
||||
short_help="Access all streaming options",
|
||||
lazy_subcommands=commands,
|
||||
epilog=examples.main,
|
||||
)
|
||||
@click.option(
|
||||
"--resume", is_flag=True, help="Resume from the last session (Not yet implemented)."
|
||||
)
|
||||
@click.pass_context
|
||||
def anilist(ctx: click.Context, resume: bool):
|
||||
"""
|
||||
The entry point for the 'anilist' command. If no subcommand is invoked,
|
||||
it launches the interactive TUI mode.
|
||||
"""
|
||||
from ...interactive.session import session
|
||||
|
||||
config = ctx.obj
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
session.load_menus_from_folder("media")
|
||||
session.run(config, resume=resume)
|
||||
70
fastanime/cli/commands/anilist/commands/auth.py
Normal file
70
fastanime/cli/commands/anilist/commands/auth.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import click
|
||||
import webbrowser
|
||||
|
||||
from .....core.config.model import AppConfig
|
||||
|
||||
|
||||
@click.command(help="Login to your AniList account to enable progress tracking.")
|
||||
@click.option("--status", "-s", is_flag=True, help="Check current login status.")
|
||||
@click.option("--logout", "-l", is_flag=True, help="Log out and erase credentials.")
|
||||
@click.pass_obj
|
||||
def auth(config: AppConfig, status: bool, logout: bool):
|
||||
"""Handles user authentication and credential management."""
|
||||
from .....core.constants import ANILIST_AUTH
|
||||
from .....libs.media_api.api import create_api_client
|
||||
from .....libs.selectors.selector import create_selector
|
||||
from ....service.auth import AuthService
|
||||
from ....service.feedback import FeedbackService
|
||||
|
||||
auth_service = AuthService("anilist")
|
||||
feedback = FeedbackService(config)
|
||||
selector = create_selector(config)
|
||||
feedback.clear_console()
|
||||
|
||||
if status:
|
||||
user_data = auth_service.get_auth()
|
||||
if user_data:
|
||||
feedback.info(f"Logged in as: {user_data.user_profile}")
|
||||
else:
|
||||
feedback.error("Not logged in.")
|
||||
return
|
||||
|
||||
if logout:
|
||||
if selector.confirm("Are you sure you want to log out and erase your token?"):
|
||||
auth_service.clear_user_profile()
|
||||
feedback.info("You have been logged out.")
|
||||
return
|
||||
|
||||
if auth_profile := auth_service.get_auth():
|
||||
if not selector.confirm(
|
||||
f"You are already logged in as {auth_profile.user_profile.name}.Would you like to relogin"
|
||||
):
|
||||
return
|
||||
api_client = create_api_client("anilist", config)
|
||||
|
||||
open_success = webbrowser.open(ANILIST_AUTH, new=2)
|
||||
if open_success:
|
||||
feedback.info("Your browser has been opened to obtain an AniList token.")
|
||||
feedback.info(f"or you can visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta].")
|
||||
else:
|
||||
feedback.warning(
|
||||
f"Failed to open the browser. Please visit the site manually [magenta][link={ANILIST_AUTH}]here[/link][/magenta]."
|
||||
)
|
||||
feedback.info(
|
||||
"After authorizing, copy the token from the address bar and paste it below."
|
||||
)
|
||||
|
||||
token = selector.ask("Enter your AniList Access Token")
|
||||
if not token:
|
||||
feedback.error("Login cancelled.")
|
||||
return
|
||||
|
||||
# Use the API client to validate the token and get profile info
|
||||
profile = api_client.authenticate(token.strip())
|
||||
|
||||
if profile:
|
||||
# If successful, use the manager to save the credentials
|
||||
auth_service.save_user_profile(profile, token)
|
||||
feedback.info(f"Successfully logged in as {profile.name}! ✨")
|
||||
else:
|
||||
feedback.error("Login failed. The token may be invalid or expired.")
|
||||
265
fastanime/cli/commands/anilist/commands/download.py
Normal file
265
fastanime/cli/commands/anilist/commands/download.py
Normal file
@@ -0,0 +1,265 @@
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
import click
|
||||
from fastanime.cli.utils.completion import anime_titles_shell_complete
|
||||
from fastanime.core.config import AppConfig
|
||||
from fastanime.core.exceptions import FastAnimeError
|
||||
from fastanime.libs.media_api.types import (
|
||||
MediaFormat,
|
||||
MediaGenre,
|
||||
MediaItem,
|
||||
MediaSeason,
|
||||
MediaSort,
|
||||
MediaStatus,
|
||||
MediaTag,
|
||||
MediaType,
|
||||
MediaYear,
|
||||
)
|
||||
|
||||
from .. import examples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
from typing_extensions import Unpack
|
||||
|
||||
class DownloadOptions(TypedDict, total=False):
|
||||
title: str | None
|
||||
episode_range: str | None
|
||||
page: int
|
||||
per_page: int | None
|
||||
season: str | None
|
||||
status: tuple[str, ...]
|
||||
status_not: tuple[str, ...]
|
||||
sort: str | None
|
||||
genres: tuple[str, ...]
|
||||
genres_not: tuple[str, ...]
|
||||
tags: tuple[str, ...]
|
||||
tags_not: tuple[str, ...]
|
||||
media_format: tuple[str, ...]
|
||||
media_type: str | None
|
||||
year: str | None
|
||||
popularity_greater: int | None
|
||||
popularity_lesser: int | None
|
||||
score_greater: int | None
|
||||
score_lesser: int | None
|
||||
start_date_greater: int | None
|
||||
start_date_lesser: int | None
|
||||
end_date_greater: int | None
|
||||
end_date_lesser: int | None
|
||||
on_list: bool | None
|
||||
yes: bool
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Search for anime on AniList and download episodes.",
|
||||
short_help="Search and download anime.",
|
||||
epilog=examples.download,
|
||||
)
|
||||
# --- Re-using all search options ---
|
||||
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||
@click.option("--page", "-p", type=click.IntRange(min=1), default=1)
|
||||
@click.option("--per-page", type=click.IntRange(min=1, max=50))
|
||||
@click.option("--season", type=click.Choice([s.value for s in MediaSeason]))
|
||||
@click.option(
|
||||
"--status", "-S", multiple=True, type=click.Choice([s.value for s in MediaStatus])
|
||||
)
|
||||
@click.option(
|
||||
"--status-not", multiple=True, type=click.Choice([s.value for s in MediaStatus])
|
||||
)
|
||||
@click.option("--sort", "-s", type=click.Choice([s.value for s in MediaSort]))
|
||||
@click.option(
|
||||
"--genres", "-g", multiple=True, type=click.Choice([g.value for g in MediaGenre])
|
||||
)
|
||||
@click.option(
|
||||
"--genres-not", multiple=True, type=click.Choice([g.value for g in MediaGenre])
|
||||
)
|
||||
@click.option(
|
||||
"--tags", "-T", multiple=True, type=click.Choice([t.value for t in MediaTag])
|
||||
)
|
||||
@click.option(
|
||||
"--tags-not", multiple=True, type=click.Choice([t.value for t in MediaTag])
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-f",
|
||||
multiple=True,
|
||||
type=click.Choice([f.value for f in MediaFormat]),
|
||||
)
|
||||
@click.option("--media-type", type=click.Choice([t.value for t in MediaType]))
|
||||
@click.option("--year", "-y", type=click.Choice([y.value for y in MediaYear]))
|
||||
@click.option("--popularity-greater", type=click.IntRange(min=0))
|
||||
@click.option("--popularity-lesser", type=click.IntRange(min=0))
|
||||
@click.option("--score-greater", type=click.IntRange(min=0, max=100))
|
||||
@click.option("--score-lesser", type=click.IntRange(min=0, max=100))
|
||||
@click.option("--start-date-greater", type=int)
|
||||
@click.option("--start-date-lesser", type=int)
|
||||
@click.option("--end-date-greater", type=int)
|
||||
@click.option("--end-date-lesser", type=int)
|
||||
@click.option("--on-list/--not-on-list", "-L/-no-L", type=bool, default=None)
|
||||
# --- Download specific options ---
|
||||
@click.option(
|
||||
"--episode-range",
|
||||
"-r",
|
||||
help="Range of episodes to download (e.g., '1-10', '5', '8:12'). Required.",
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"--yes",
|
||||
"-Y",
|
||||
is_flag=True,
|
||||
help="Automatically download from all found anime without prompting for selection.",
|
||||
)
|
||||
@click.pass_obj
|
||||
def download(config: AppConfig, **options: "Unpack[DownloadOptions]"):
|
||||
from fastanime.cli.service.download.service import DownloadService
|
||||
from fastanime.cli.service.feedback import FeedbackService
|
||||
from fastanime.cli.service.registry import MediaRegistryService
|
||||
from fastanime.cli.service.watch_history import WatchHistoryService
|
||||
from fastanime.cli.utils.parser import parse_episode_range
|
||||
from fastanime.libs.media_api.api import create_api_client
|
||||
from fastanime.libs.media_api.params import MediaSearchParams
|
||||
from fastanime.libs.provider.anime.provider import create_provider
|
||||
from fastanime.libs.selectors import create_selector
|
||||
from rich.progress import Progress
|
||||
|
||||
feedback = FeedbackService(config)
|
||||
selector = create_selector(config)
|
||||
media_api = create_api_client(config.general.media_api, config)
|
||||
provider = create_provider(config.general.provider)
|
||||
registry = MediaRegistryService(config.general.media_api, config.media_registry)
|
||||
watch_history = WatchHistoryService(config, registry, media_api)
|
||||
download_service = DownloadService(config, registry, media_api, provider)
|
||||
|
||||
try:
|
||||
sort_val = options.get("sort")
|
||||
status_val = options.get("status")
|
||||
status_not_val = options.get("status_not")
|
||||
genres_val = options.get("genres")
|
||||
genres_not_val = options.get("genres_not")
|
||||
tags_val = options.get("tags")
|
||||
tags_not_val = options.get("tags_not")
|
||||
media_format_val = options.get("media_format")
|
||||
media_type_val = options.get("media_type")
|
||||
season_val = options.get("season")
|
||||
year_val = options.get("year")
|
||||
|
||||
search_params = MediaSearchParams(
|
||||
query=options.get("title"),
|
||||
page=options.get("page", 1),
|
||||
per_page=options.get("per_page"),
|
||||
sort=MediaSort(sort_val) if sort_val else None,
|
||||
status_in=[MediaStatus(s) for s in status_val] if status_val else None,
|
||||
status_not_in=[MediaStatus(s) for s in status_not_val]
|
||||
if status_not_val
|
||||
else None,
|
||||
genre_in=[MediaGenre(g) for g in genres_val] if genres_val else None,
|
||||
genre_not_in=[MediaGenre(g) for g in genres_not_val]
|
||||
if genres_not_val
|
||||
else None,
|
||||
tag_in=[MediaTag(t) for t in tags_val] if tags_val else None,
|
||||
tag_not_in=[MediaTag(t) for t in tags_not_val] if tags_not_val else None,
|
||||
format_in=[MediaFormat(f) for f in media_format_val]
|
||||
if media_format_val
|
||||
else None,
|
||||
type=MediaType(media_type_val) if media_type_val else None,
|
||||
season=MediaSeason(season_val) if season_val else None,
|
||||
seasonYear=int(year_val) if year_val else None,
|
||||
popularity_greater=options.get("popularity_greater"),
|
||||
popularity_lesser=options.get("popularity_lesser"),
|
||||
averageScore_greater=options.get("score_greater"),
|
||||
averageScore_lesser=options.get("score_lesser"),
|
||||
startDate_greater=options.get("start_date_greater"),
|
||||
startDate_lesser=options.get("start_date_lesser"),
|
||||
endDate_greater=options.get("end_date_greater"),
|
||||
endDate_lesser=options.get("end_date_lesser"),
|
||||
on_list=options.get("on_list"),
|
||||
)
|
||||
|
||||
with Progress() as progress:
|
||||
progress.add_task("Searching AniList...", total=None)
|
||||
search_result = media_api.search_media(search_params)
|
||||
|
||||
if not search_result or not search_result.media:
|
||||
raise FastAnimeError("No anime found matching your search criteria.")
|
||||
|
||||
anime_to_download: List[MediaItem]
|
||||
if options.get("yes"):
|
||||
anime_to_download = search_result.media
|
||||
else:
|
||||
choice_map: Dict[str, MediaItem] = {
|
||||
(item.title.english or item.title.romaji or f"ID: {item.id}"): item
|
||||
for item in search_result.media
|
||||
}
|
||||
preview_command = None
|
||||
if config.general.preview != "none":
|
||||
from ....utils.preview import create_preview_context
|
||||
|
||||
with create_preview_context() as preview_ctx:
|
||||
preview_command = preview_ctx.get_anime_preview(
|
||||
list(choice_map.values()),
|
||||
list(choice_map.keys()),
|
||||
config,
|
||||
)
|
||||
selected_titles = selector.choose_multiple(
|
||||
"Select anime to download",
|
||||
list(choice_map.keys()),
|
||||
preview=preview_command,
|
||||
)
|
||||
else:
|
||||
selected_titles = selector.choose_multiple(
|
||||
"Select anime to download",
|
||||
list(choice_map.keys()),
|
||||
)
|
||||
if not selected_titles:
|
||||
feedback.warning("No anime selected. Aborting download.")
|
||||
return
|
||||
anime_to_download = [choice_map[title] for title in selected_titles]
|
||||
|
||||
total_downloaded = 0
|
||||
episode_range_str = options.get("episode_range")
|
||||
if not episode_range_str:
|
||||
raise FastAnimeError("--episode-range is required.")
|
||||
|
||||
for media_item in anime_to_download:
|
||||
watch_history.add_media_to_list_if_not_present(media_item)
|
||||
|
||||
available_episodes = [str(i + 1) for i in range(media_item.episodes or 0)]
|
||||
if not available_episodes:
|
||||
feedback.warning(
|
||||
f"No episode information for '{media_item.title.english}', skipping."
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
episodes_to_download = list(
|
||||
parse_episode_range(episode_range_str, available_episodes)
|
||||
)
|
||||
if not episodes_to_download:
|
||||
feedback.warning(
|
||||
f"Episode range '{episode_range_str}' resulted in no episodes for '{media_item.title.english}'."
|
||||
)
|
||||
continue
|
||||
|
||||
feedback.info(
|
||||
f"Preparing to download {len(episodes_to_download)} episodes for '{media_item.title.english}'."
|
||||
)
|
||||
download_service.download_episodes_sync(
|
||||
media_item, episodes_to_download
|
||||
)
|
||||
total_downloaded += len(episodes_to_download)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
feedback.error(
|
||||
f"Invalid episode range for '{media_item.title.english}': {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
feedback.success(
|
||||
f"Finished. Successfully downloaded a total of {total_downloaded} episodes."
|
||||
)
|
||||
|
||||
except FastAnimeError as e:
|
||||
feedback.error("Download command failed", str(e))
|
||||
except Exception as e:
|
||||
feedback.error("An unexpected error occurred", str(e))
|
||||
211
fastanime/cli/commands/anilist/commands/downloads.py
Normal file
211
fastanime/cli/commands/anilist/commands/downloads.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from .....core.config import AppConfig
|
||||
from .....libs.media_api.params import MediaSearchParams
|
||||
from .....libs.media_api.types import (
|
||||
MediaFormat,
|
||||
MediaGenre,
|
||||
MediaSort,
|
||||
UserMediaListStatus,
|
||||
)
|
||||
from ....service.feedback import FeedbackService
|
||||
from ....service.registry.service import MediaRegistryService
|
||||
|
||||
|
||||
@click.command(help="Search through the local media registry")
|
||||
@click.argument("query", required=False)
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(
|
||||
[s.value for s in UserMediaListStatus],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Filter by watch status",
|
||||
)
|
||||
@click.option(
|
||||
"--genre", multiple=True, help="Filter by genre (can be used multiple times)"
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
type=click.Choice(
|
||||
[
|
||||
f.value
|
||||
for f in MediaFormat
|
||||
if f not in [MediaFormat.MANGA, MediaFormat.NOVEL, MediaFormat.ONE_SHOT]
|
||||
],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Filter by format",
|
||||
)
|
||||
@click.option("--year", type=int, help="Filter by release year")
|
||||
@click.option("--min-score", type=float, help="Minimum average score (0.0 - 10.0)")
|
||||
@click.option("--max-score", type=float, help="Maximum average score (0.0 - 10.0)")
|
||||
@click.option(
|
||||
"--sort",
|
||||
type=click.Choice(
|
||||
["title", "score", "popularity", "year", "episodes", "updated"],
|
||||
case_sensitive=False,
|
||||
),
|
||||
default="title",
|
||||
help="Sort results by field",
|
||||
)
|
||||
@click.option("--limit", type=int, default=20, help="Maximum number of results to show")
|
||||
@click.option(
|
||||
"--json", "output_json", is_flag=True, help="Output results in JSON format"
|
||||
)
|
||||
@click.option(
|
||||
"--api",
|
||||
default="anilist",
|
||||
type=click.Choice(["anilist"], case_sensitive=False),
|
||||
help="Media API registry to search",
|
||||
)
|
||||
@click.pass_obj
|
||||
def downloads(
|
||||
config: AppConfig,
|
||||
query: str | None,
|
||||
status: str | None,
|
||||
genre: tuple[str, ...],
|
||||
format: str | None,
|
||||
year: int | None,
|
||||
min_score: float | None,
|
||||
max_score: float | None,
|
||||
sort: str,
|
||||
limit: int,
|
||||
output_json: bool,
|
||||
api: str,
|
||||
):
|
||||
"""
|
||||
Search through your local media registry.
|
||||
|
||||
You can search by title and filter by various criteria like status,
|
||||
genre, format, year, and score range.
|
||||
"""
|
||||
feedback = FeedbackService(config)
|
||||
if not has_user_input(click.get_current_context()):
|
||||
from ....interactive.session import session
|
||||
from ....interactive.state import MediaApiState, MenuName, State
|
||||
|
||||
# Create initial state with search results
|
||||
initial_state = [State(menu_name=MenuName.DOWNLOADS)]
|
||||
|
||||
session.load_menus_from_folder("media")
|
||||
session.run(config, history=initial_state)
|
||||
|
||||
registry_service = MediaRegistryService(api, config.media_registry)
|
||||
|
||||
search_params = _build_search_params(
|
||||
query, status, genre, format, year, min_score, max_score, sort, limit
|
||||
)
|
||||
|
||||
with feedback.progress("Searching local registry..."):
|
||||
result = registry_service.search_for_media(search_params)
|
||||
|
||||
if not result or not result.media:
|
||||
feedback.info("No Results", "No media found matching your criteria")
|
||||
return
|
||||
|
||||
if output_json:
|
||||
print(json.dumps(result.model_dump(mode="json"), indent=2))
|
||||
return
|
||||
|
||||
from ....interactive.session import session
|
||||
from ....interactive.state import MediaApiState, MenuName, State
|
||||
|
||||
feedback.info(
|
||||
f"Found {len(result.media)} anime matching your search. Launching interactive mode..."
|
||||
)
|
||||
|
||||
# Create initial state with search results
|
||||
initial_state = [
|
||||
State(menu_name=MenuName.DOWNLOADS),
|
||||
State(
|
||||
menu_name=MenuName.RESULTS,
|
||||
media_api=MediaApiState(
|
||||
search_result={
|
||||
media_item.id: media_item for media_item in result.media
|
||||
},
|
||||
search_params=search_params,
|
||||
page_info=result.page_info,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
session.load_menus_from_folder("media")
|
||||
session.run(config, history=initial_state)
|
||||
|
||||
|
||||
def _build_search_params(
|
||||
query: str | None,
|
||||
status: str | None,
|
||||
genre: tuple[str, ...],
|
||||
format_str: str | None,
|
||||
year: int | None,
|
||||
min_score: float | None,
|
||||
max_score: float | None,
|
||||
sort: str,
|
||||
limit: int,
|
||||
) -> MediaSearchParams:
|
||||
"""Build MediaSearchParams from command options for local filtering."""
|
||||
sort_map = {
|
||||
"title": MediaSort.TITLE_ROMAJI,
|
||||
"score": MediaSort.SCORE_DESC,
|
||||
"popularity": MediaSort.POPULARITY_DESC,
|
||||
"year": MediaSort.START_DATE_DESC,
|
||||
"episodes": MediaSort.EPISODES_DESC,
|
||||
"updated": MediaSort.UPDATED_AT_DESC,
|
||||
}
|
||||
|
||||
# Safely convert strings to enums
|
||||
format_enum = next(
|
||||
(f for f in MediaFormat if f.value.lower() == (format_str or "").lower()), None
|
||||
)
|
||||
genre_enums = [
|
||||
g for g_str in genre for g in MediaGenre if g.value.lower() == g_str.lower()
|
||||
]
|
||||
|
||||
# Note: Local search handles status separately as it's part of the index, not MediaItem
|
||||
|
||||
return MediaSearchParams(
|
||||
query=query,
|
||||
per_page=limit,
|
||||
sort=[sort_map.get(sort.lower(), MediaSort.TITLE_ROMAJI)],
|
||||
averageScore_greater=int(min_score * 10) if min_score is not None else None,
|
||||
averageScore_lesser=int(max_score * 10) if max_score is not None else None,
|
||||
genre_in=genre_enums or None,
|
||||
format_in=[format_enum] if format_enum else None,
|
||||
seasonYear=year,
|
||||
)
|
||||
|
||||
|
||||
def has_user_input(ctx: click.Context) -> bool:
|
||||
"""
|
||||
Checks if any command-line options or arguments were provided by the user
|
||||
by comparing the given values to their default values.
|
||||
|
||||
This handles all parameter types including flags, multiple options,
|
||||
and arguments with no default.
|
||||
"""
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 3:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
for param in ctx.command.params:
|
||||
# Get the value for the parameter from the context.
|
||||
# This will be the user-provided value or the default.
|
||||
value = ctx.params.get(param.name)
|
||||
|
||||
# We need to explicitly check if a value was provided by the user.
|
||||
# The simplest way to do this is to compare it to its default.
|
||||
if value != param.default:
|
||||
# If the value is different from the default, the user
|
||||
# must have provided it.
|
||||
return True
|
||||
|
||||
# If the loop completes without finding any non-default values,
|
||||
# then no user input was given.
|
||||
return False
|
||||
56
fastanime/cli/commands/anilist/commands/notifications.py
Normal file
56
fastanime/cli/commands/anilist/commands/notifications.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import click
|
||||
from fastanime.core.config import AppConfig
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
|
||||
@click.command(help="Check for new AniList notifications (e.g., for airing episodes).")
|
||||
@click.pass_obj
|
||||
def notifications(config: AppConfig):
|
||||
"""
|
||||
Displays unread notifications from AniList.
|
||||
Running this command will also mark the notifications as read on the AniList website.
|
||||
"""
|
||||
from fastanime.cli.service.feedback import FeedbackService
|
||||
from fastanime.libs.media_api.api import create_api_client
|
||||
|
||||
from ....service.auth import AuthService
|
||||
|
||||
feedback = FeedbackService(config)
|
||||
console = Console()
|
||||
auth = AuthService(config.general.media_api)
|
||||
api_client = create_api_client(config.general.media_api, config)
|
||||
if profile := auth.get_auth():
|
||||
api_client.authenticate(profile.token)
|
||||
|
||||
if not api_client.is_authenticated():
|
||||
feedback.error(
|
||||
"Authentication Required", "Please log in with 'fastanime anilist auth'."
|
||||
)
|
||||
return
|
||||
|
||||
with feedback.progress("Fetching notifications..."):
|
||||
notifs = api_client.get_notifications()
|
||||
|
||||
if not notifs:
|
||||
feedback.success("All caught up!", "You have no new notifications.")
|
||||
return
|
||||
|
||||
table = Table(
|
||||
title="🔔 AniList Notifications", show_header=True, header_style="bold magenta"
|
||||
)
|
||||
table.add_column("Date", style="dim", width=12)
|
||||
table.add_column("Anime Title", style="cyan")
|
||||
table.add_column("Details", style="green")
|
||||
|
||||
for notif in sorted(notifs, key=lambda n: n.created_at, reverse=True):
|
||||
title = notif.media.title.english or notif.media.title.romaji or "Unknown"
|
||||
date_str = notif.created_at.strftime("%Y-%m-%d")
|
||||
details = f"Episode {notif.episode} has aired!"
|
||||
|
||||
table.add_row(date_str, title, details)
|
||||
|
||||
console.print(table)
|
||||
feedback.info(
|
||||
"Notifications have been marked as read on AniList.",
|
||||
)
|
||||
334
fastanime/cli/commands/anilist/commands/search.py
Normal file
334
fastanime/cli/commands/anilist/commands/search.py
Normal file
@@ -0,0 +1,334 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
from .....core.config import AppConfig
|
||||
from .....core.exceptions import FastAnimeError
|
||||
from .....libs.media_api.types import (
|
||||
MediaFormat,
|
||||
MediaGenre,
|
||||
MediaSeason,
|
||||
MediaSort,
|
||||
MediaStatus,
|
||||
MediaTag,
|
||||
MediaType,
|
||||
MediaYear,
|
||||
)
|
||||
from ....utils.completion import anime_titles_shell_complete
|
||||
from .. import examples
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
from typing_extensions import Unpack
|
||||
|
||||
class SearchOptions(TypedDict, total=False):
|
||||
title: str | None
|
||||
dump_json: bool
|
||||
page: int
|
||||
per_page: int | None
|
||||
season: str | None
|
||||
status: tuple[str, ...]
|
||||
status_not: tuple[str, ...]
|
||||
sort: str | None
|
||||
genres: tuple[str, ...]
|
||||
genres_not: tuple[str, ...]
|
||||
tags: tuple[str, ...]
|
||||
tags_not: tuple[str, ...]
|
||||
media_format: tuple[str, ...]
|
||||
media_type: str | None
|
||||
year: str | None
|
||||
popularity_greater: int | None
|
||||
popularity_lesser: int | None
|
||||
score_greater: int | None
|
||||
score_lesser: int | None
|
||||
start_date_greater: int | None
|
||||
start_date_lesser: int | None
|
||||
end_date_greater: int | None
|
||||
end_date_lesser: int | None
|
||||
on_list: bool | None
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Search for anime using anilists api and get top ~50 results",
|
||||
short_help="Search for anime",
|
||||
epilog=examples.search,
|
||||
)
|
||||
@click.option("--title", "-t", shell_complete=anime_titles_shell_complete)
|
||||
@click.option(
|
||||
"--dump-json",
|
||||
"-d",
|
||||
is_flag=True,
|
||||
help="Only print out the results dont open anilist menu",
|
||||
)
|
||||
@click.option(
|
||||
"--page",
|
||||
"-p",
|
||||
type=click.IntRange(min=1),
|
||||
default=1,
|
||||
help="Page number for pagination",
|
||||
)
|
||||
@click.option(
|
||||
"--per-page",
|
||||
type=click.IntRange(min=1, max=50),
|
||||
help="Number of results per page (max 50)",
|
||||
)
|
||||
@click.option(
|
||||
"--season",
|
||||
help="The season the media was released",
|
||||
type=click.Choice([season.value for season in MediaSeason]),
|
||||
)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-S",
|
||||
help="The media status of the anime",
|
||||
multiple=True,
|
||||
type=click.Choice([status.value for status in MediaStatus]),
|
||||
)
|
||||
@click.option(
|
||||
"--status-not",
|
||||
help="Exclude media with these statuses",
|
||||
multiple=True,
|
||||
type=click.Choice([status.value for status in MediaStatus]),
|
||||
)
|
||||
@click.option(
|
||||
"--sort",
|
||||
"-s",
|
||||
help="What to sort the search results on",
|
||||
type=click.Choice([sort.value for sort in MediaSort]),
|
||||
)
|
||||
@click.option(
|
||||
"--genres",
|
||||
"-g",
|
||||
multiple=True,
|
||||
help="the genres to filter by",
|
||||
type=click.Choice([genre.value for genre in MediaGenre]),
|
||||
)
|
||||
@click.option(
|
||||
"--genres-not",
|
||||
multiple=True,
|
||||
help="Exclude these genres",
|
||||
type=click.Choice([genre.value for genre in MediaGenre]),
|
||||
)
|
||||
@click.option(
|
||||
"--tags",
|
||||
"-T",
|
||||
multiple=True,
|
||||
help="the tags to filter by",
|
||||
type=click.Choice([tag.value for tag in MediaTag]),
|
||||
)
|
||||
@click.option(
|
||||
"--tags-not",
|
||||
multiple=True,
|
||||
help="Exclude these tags",
|
||||
type=click.Choice([tag.value for tag in MediaTag]),
|
||||
)
|
||||
@click.option(
|
||||
"--media-format",
|
||||
"-f",
|
||||
multiple=True,
|
||||
help="Media format",
|
||||
type=click.Choice([format.value for format in MediaFormat]),
|
||||
)
|
||||
@click.option(
|
||||
"--media-type",
|
||||
help="Media type (ANIME or MANGA)",
|
||||
type=click.Choice([media_type.value for media_type in MediaType]),
|
||||
)
|
||||
@click.option(
|
||||
"--year",
|
||||
"-y",
|
||||
type=click.Choice([year.value for year in MediaYear]),
|
||||
help="the year the media was released",
|
||||
)
|
||||
@click.option(
|
||||
"--popularity-greater",
|
||||
type=click.IntRange(min=0),
|
||||
help="Minimum popularity score",
|
||||
)
|
||||
@click.option(
|
||||
"--popularity-lesser",
|
||||
type=click.IntRange(min=0),
|
||||
help="Maximum popularity score",
|
||||
)
|
||||
@click.option(
|
||||
"--score-greater",
|
||||
type=click.IntRange(min=0, max=100),
|
||||
help="Minimum average score (0-100)",
|
||||
)
|
||||
@click.option(
|
||||
"--score-lesser",
|
||||
type=click.IntRange(min=0, max=100),
|
||||
help="Maximum average score (0-100)",
|
||||
)
|
||||
@click.option(
|
||||
"--start-date-greater",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Minimum start date (YYYYMMDD format, e.g., 20240101)",
|
||||
)
|
||||
@click.option(
|
||||
"--start-date-lesser",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Maximum start date (YYYYMMDD format, e.g., 20241231)",
|
||||
)
|
||||
@click.option(
|
||||
"--end-date-greater",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Minimum end date (YYYYMMDD format, e.g., 20240101)",
|
||||
)
|
||||
@click.option(
|
||||
"--end-date-lesser",
|
||||
type=click.IntRange(min=10000101, max=99991231),
|
||||
help="Maximum end date (YYYYMMDD format, e.g., 20241231)",
|
||||
)
|
||||
@click.option(
|
||||
"--on-list/--not-on-list",
|
||||
"-L/-no-L",
|
||||
help="Whether the anime should be in your list or not",
|
||||
type=bool,
|
||||
)
|
||||
@click.pass_obj
|
||||
def search(config: AppConfig, **options: "Unpack[SearchOptions]"):
|
||||
import json
|
||||
|
||||
from rich.progress import Progress
|
||||
|
||||
from .....libs.media_api.api import create_api_client
|
||||
from .....libs.media_api.params import MediaSearchParams
|
||||
from ....service.feedback import FeedbackService
|
||||
|
||||
feedback = FeedbackService(config)
|
||||
|
||||
try:
|
||||
# Create API client
|
||||
api_client = create_api_client(config.general.media_api, config)
|
||||
|
||||
# Extract options
|
||||
title = options.get("title")
|
||||
dump_json = options.get("dump_json", False)
|
||||
page = options.get("page", 1)
|
||||
per_page = options.get("per_page") or config.anilist.per_page or 50
|
||||
season = options.get("season")
|
||||
status = options.get("status", ())
|
||||
status_not = options.get("status_not", ())
|
||||
sort = options.get("sort")
|
||||
genres = options.get("genres", ())
|
||||
genres_not = options.get("genres_not", ())
|
||||
tags = options.get("tags", ())
|
||||
tags_not = options.get("tags_not", ())
|
||||
media_format = options.get("media_format", ())
|
||||
media_type = options.get("media_type")
|
||||
year = options.get("year")
|
||||
popularity_greater = options.get("popularity_greater")
|
||||
popularity_lesser = options.get("popularity_lesser")
|
||||
score_greater = options.get("score_greater")
|
||||
score_lesser = options.get("score_lesser")
|
||||
start_date_greater = options.get("start_date_greater")
|
||||
start_date_lesser = options.get("start_date_lesser")
|
||||
end_date_greater = options.get("end_date_greater")
|
||||
end_date_lesser = options.get("end_date_lesser")
|
||||
on_list = options.get("on_list")
|
||||
|
||||
# Validate logical relationships
|
||||
if (
|
||||
score_greater is not None
|
||||
and score_lesser is not None
|
||||
and score_greater > score_lesser
|
||||
):
|
||||
raise FastAnimeError("Minimum score cannot be higher than maximum score")
|
||||
|
||||
if (
|
||||
popularity_greater is not None
|
||||
and popularity_lesser is not None
|
||||
and popularity_greater > popularity_lesser
|
||||
):
|
||||
raise FastAnimeError(
|
||||
"Minimum popularity cannot be higher than maximum popularity"
|
||||
)
|
||||
|
||||
if (
|
||||
start_date_greater is not None
|
||||
and start_date_lesser is not None
|
||||
and start_date_greater > start_date_lesser
|
||||
):
|
||||
raise FastAnimeError(
|
||||
"Start date greater cannot be later than start date lesser"
|
||||
)
|
||||
|
||||
if (
|
||||
end_date_greater is not None
|
||||
and end_date_lesser is not None
|
||||
and end_date_greater > end_date_lesser
|
||||
):
|
||||
raise FastAnimeError(
|
||||
"End date greater cannot be later than end date lesser"
|
||||
)
|
||||
|
||||
# Build search parameters
|
||||
search_params = MediaSearchParams(
|
||||
query=title,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort=MediaSort(sort) if sort else None,
|
||||
status_in=[MediaStatus(s) for s in status] if status else None,
|
||||
status_not_in=[MediaStatus(s) for s in status_not] if status_not else None,
|
||||
genre_in=[MediaGenre(g) for g in genres] if genres else None,
|
||||
genre_not_in=[MediaGenre(g) for g in genres_not] if genres_not else None,
|
||||
tag_in=[MediaTag(t) for t in tags] if tags else None,
|
||||
tag_not_in=[MediaTag(t) for t in tags_not] if tags_not else None,
|
||||
format_in=[MediaFormat(f) for f in media_format] if media_format else None,
|
||||
type=MediaType(media_type) if media_type else None,
|
||||
season=MediaSeason(season) if season else None,
|
||||
seasonYear=int(year) if year else None,
|
||||
popularity_greater=popularity_greater,
|
||||
popularity_lesser=popularity_lesser,
|
||||
averageScore_greater=score_greater,
|
||||
averageScore_lesser=score_lesser,
|
||||
startDate_greater=start_date_greater,
|
||||
startDate_lesser=start_date_lesser,
|
||||
endDate_greater=end_date_greater,
|
||||
endDate_lesser=end_date_lesser,
|
||||
on_list=on_list,
|
||||
)
|
||||
|
||||
# Search for anime
|
||||
with Progress() as progress:
|
||||
progress.add_task("Searching anime...", total=None)
|
||||
search_result = api_client.search_media(search_params)
|
||||
|
||||
if not search_result or not search_result.media:
|
||||
raise FastAnimeError("No anime found matching your search criteria")
|
||||
|
||||
if dump_json:
|
||||
# Use Pydantic's built-in serialization
|
||||
print(json.dumps(search_result.model_dump(mode="json")))
|
||||
else:
|
||||
# Launch interactive session for browsing results
|
||||
from ....interactive.session import session
|
||||
from ....interactive.state import MediaApiState, MenuName, State
|
||||
|
||||
feedback.info(
|
||||
f"Found {len(search_result.media)} anime matching your search. Launching interactive mode..."
|
||||
)
|
||||
|
||||
# Create initial state with search results
|
||||
initial_state = State(
|
||||
menu_name=MenuName.RESULTS,
|
||||
media_api=MediaApiState(
|
||||
search_result={
|
||||
media_item.id: media_item for media_item in search_result.media
|
||||
},
|
||||
search_params=search_params,
|
||||
page_info=search_result.page_info,
|
||||
),
|
||||
)
|
||||
|
||||
session.load_menus_from_folder("media")
|
||||
session.run(config, history=[initial_state])
|
||||
|
||||
except FastAnimeError as e:
|
||||
feedback.error("Search failed", str(e))
|
||||
raise click.Abort()
|
||||
except Exception as e:
|
||||
feedback.error("Unexpected error occurred", str(e))
|
||||
raise click.Abort()
|
||||
90
fastanime/cli/commands/anilist/commands/stats.py
Normal file
90
fastanime/cli/commands/anilist/commands/stats.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastanime.core.config import AppConfig
|
||||
|
||||
|
||||
@click.command(help="Print out your anilist stats")
|
||||
@click.pass_obj
|
||||
def stats(config: "AppConfig"):
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from rich.console import Console
|
||||
from rich.markdown import Markdown
|
||||
from rich.panel import Panel
|
||||
|
||||
from .....libs.media_api.api import create_api_client
|
||||
from ....service.auth import AuthService
|
||||
from ....service.feedback import FeedbackService
|
||||
|
||||
console = Console()
|
||||
|
||||
feedback = FeedbackService(config)
|
||||
auth = AuthService(config.general.media_api)
|
||||
|
||||
media_api_client = create_api_client(config.general.media_api, config)
|
||||
|
||||
try:
|
||||
# Check authentication
|
||||
|
||||
if profile := auth.get_auth():
|
||||
if not media_api_client.authenticate(profile.token):
|
||||
feedback.error(
|
||||
"Authentication Required",
|
||||
f"You must be logged in to {config.general.media_api} to sync your media list.",
|
||||
)
|
||||
feedback.info(
|
||||
"Run this command to authenticate:",
|
||||
f"fastanime {config.general.media_api} auth",
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
# Check if kitten is available for image display
|
||||
KITTEN_EXECUTABLE = shutil.which("kitten")
|
||||
if not KITTEN_EXECUTABLE:
|
||||
feedback.warning(
|
||||
"Kitten not found - profile image will not be displayed"
|
||||
)
|
||||
else:
|
||||
# Display profile image using kitten icat
|
||||
if profile.user_profile.avatar_url:
|
||||
console.clear()
|
||||
image_x = int(console.size.width * 0.1)
|
||||
image_y = int(console.size.height * 0.1)
|
||||
img_w = console.size.width // 3
|
||||
img_h = console.size.height // 3
|
||||
|
||||
image_process = subprocess.run(
|
||||
[
|
||||
KITTEN_EXECUTABLE,
|
||||
"icat",
|
||||
"--clear",
|
||||
"--place",
|
||||
f"{img_w}x{img_h}@{image_x}x{image_y}",
|
||||
profile.user_profile.avatar_url,
|
||||
],
|
||||
check=False,
|
||||
)
|
||||
|
||||
if image_process.returncode != 0:
|
||||
feedback.warning("Failed to display profile image")
|
||||
|
||||
# Display user information
|
||||
about_text = getattr(profile, "about", "") or "No description available"
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
Markdown(about_text),
|
||||
title=f"📊 {profile.user_profile.name}'s Profile",
|
||||
)
|
||||
)
|
||||
|
||||
# You can add more stats here if the API provides them
|
||||
feedback.success("User profile displayed successfully")
|
||||
|
||||
except Exception as e:
|
||||
feedback.error("Unexpected error occurred", str(e))
|
||||
raise click.Abort()
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you completed")
|
||||
@click.pass_obj
|
||||
def completed(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("COMPLETED")
|
||||
if not anime_list or not anime_list[1]:
|
||||
return
|
||||
if not anime_list[0]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastanime.cli.config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you dropped")
|
||||
@click.pass_obj
|
||||
def dropped(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("DROPPED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
169
fastanime/cli/commands/anilist/examples.py
Normal file
169
fastanime/cli/commands/anilist/examples.py
Normal file
@@ -0,0 +1,169 @@
|
||||
download = """
|
||||
\b
|
||||
\b\bExamples:
|
||||
# Basic download by title
|
||||
fastanime anilist download -t "Attack on Titan"
|
||||
\b
|
||||
# Download specific episodes
|
||||
fastanime anilist download -t "One Piece" --episode-range "1-10"
|
||||
\b
|
||||
# Download single episode
|
||||
fastanime anilist download -t "Death Note" --episode-range "1"
|
||||
\b
|
||||
# Download multiple specific episodes
|
||||
fastanime anilist download -t "Naruto" --episode-range "1,5,10"
|
||||
\b
|
||||
# Download with quality preference
|
||||
fastanime anilist download -t "Death Note" --quality 1080 --episode-range "1-5"
|
||||
\b
|
||||
# Download with multiple filters
|
||||
fastanime anilist download -g Action -T Isekai --score-greater 80 --status RELEASING
|
||||
\b
|
||||
# Download with concurrent downloads
|
||||
fastanime anilist download -t "Demon Slayer" --episode-range "1-5" --max-concurrent 3
|
||||
\b
|
||||
# Force redownload existing episodes
|
||||
fastanime anilist download -t "Your Name" --episode-range "1" --force-redownload
|
||||
\b
|
||||
# Download from a specific season and year
|
||||
fastanime anilist download --season WINTER --year 2024 -s POPULARITY_DESC
|
||||
\b
|
||||
# Download with genre filtering
|
||||
fastanime anilist download -g Action -g Adventure --score-greater 75
|
||||
\b
|
||||
# Download only completed series
|
||||
fastanime anilist download -g Fantasy --status FINISHED --score-greater 75
|
||||
\b
|
||||
# Download movies only
|
||||
fastanime anilist download -F MOVIE -s SCORE_DESC --quality best
|
||||
"""
|
||||
|
||||
|
||||
search = """
|
||||
\b
|
||||
\b\bExamples:
|
||||
# Basic search by title
|
||||
fastanime anilist search -t "Attack on Titan"
|
||||
\b
|
||||
# Search with multiple filters
|
||||
fastanime anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
|
||||
\b
|
||||
# Get anime with the tag of isekai
|
||||
fastanime anilist search -T isekai
|
||||
\b
|
||||
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
|
||||
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
|
||||
\b
|
||||
# Get anime of 2024 season WINTER
|
||||
fastanime anilist search -y 2024 --season WINTER
|
||||
\b
|
||||
# Get anime genre action and tag isekai,magic
|
||||
fastanime anilist search -g Action -T Isekai -T Magic
|
||||
\b
|
||||
# Get anime of 2024 thats finished airing
|
||||
fastanime anilist search -y 2024 -S FINISHED
|
||||
\b
|
||||
# Get the most favourite anime movies
|
||||
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
|
||||
\b
|
||||
# Search with score and popularity filters
|
||||
fastanime anilist search --score-greater 80 --popularity-greater 50000
|
||||
\b
|
||||
# Search excluding certain genres and tags
|
||||
fastanime anilist search --genres-not Ecchi --tags-not "Hentai"
|
||||
\b
|
||||
# Search with date ranges (YYYYMMDD format)
|
||||
fastanime anilist search --start-date-greater 20200101 --start-date-lesser 20241231
|
||||
\b
|
||||
# Get only TV series, exclude certain statuses
|
||||
fastanime anilist search -f TV --status-not CANCELLED --status-not HIATUS
|
||||
\b
|
||||
# Paginated search with custom page size
|
||||
fastanime anilist search -g Action --page 2 --per-page 25
|
||||
\b
|
||||
# Search for manga specifically
|
||||
fastanime anilist search --media-type MANGA -g Fantasy
|
||||
\b
|
||||
# Complex search with multiple criteria
|
||||
fastanime anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
|
||||
\b
|
||||
# Dump search results as JSON instead of interactive mode
|
||||
fastanime anilist search -g Action --dump-json
|
||||
"""
|
||||
|
||||
|
||||
main = """
|
||||
\b
|
||||
\b\bExamples:
|
||||
# ---- search ----
|
||||
\b
|
||||
# Basic search by title
|
||||
fastanime anilist search -t "Attack on Titan"
|
||||
\b
|
||||
# Search with multiple filters
|
||||
fastanime anilist search -g Action -T Isekai --score-greater 75 --status RELEASING
|
||||
\b
|
||||
# Get anime with the tag of isekai
|
||||
fastanime anilist search -T isekai
|
||||
\b
|
||||
# Get anime of 2024 and sort by popularity, finished or releasing, not in your list
|
||||
fastanime anilist search -y 2024 -s POPULARITY_DESC --status RELEASING --status FINISHED --not-on-list
|
||||
\b
|
||||
# Get anime of 2024 season WINTER
|
||||
fastanime anilist search -y 2024 --season WINTER
|
||||
\b
|
||||
# Get anime genre action and tag isekai,magic
|
||||
fastanime anilist search -g Action -T Isekai -T Magic
|
||||
\b
|
||||
# Get anime of 2024 thats finished airing
|
||||
fastanime anilist search -y 2024 -S FINISHED
|
||||
\b
|
||||
# Get the most favourite anime movies
|
||||
fastanime anilist search -f MOVIE -s FAVOURITES_DESC
|
||||
\b
|
||||
# Search with score and popularity filters
|
||||
fastanime anilist search --score-greater 80 --popularity-greater 50000
|
||||
\b
|
||||
# Search excluding certain genres and tags
|
||||
fastanime anilist search --genres-not Ecchi --tags-not "Hentai"
|
||||
\b
|
||||
# Search with date ranges (YYYYMMDD format)
|
||||
fastanime anilist search --start-date-greater 20200101 --start-date-lesser 20241231
|
||||
\b
|
||||
# Get only TV series, exclude certain statuses
|
||||
fastanime anilist search -f TV --status-not CANCELLED --status-not HIATUS
|
||||
\b
|
||||
# Paginated search with custom page size
|
||||
fastanime anilist search -g Action --page 2 --per-page 25
|
||||
\b
|
||||
# Search for manga specifically
|
||||
fastanime anilist search --media-type MANGA -g Fantasy
|
||||
\b
|
||||
# Complex search with multiple criteria
|
||||
fastanime anilist search -t "demon" -g Action -g Supernatural --score-greater 70 --year 2020 -s SCORE_DESC
|
||||
\b
|
||||
# Dump search results as JSON instead of interactive mode
|
||||
fastanime anilist search -g Action --dump-json
|
||||
\b
|
||||
# ---- login ----
|
||||
\b
|
||||
# To sign in just run
|
||||
fastanime anilist auth
|
||||
\b
|
||||
# To check your login status
|
||||
fastanime anilist auth --status
|
||||
\b
|
||||
# To log out and erase credentials
|
||||
fastanime anilist auth --logout
|
||||
\b
|
||||
# ---- notifier ----
|
||||
\b
|
||||
# basic form
|
||||
fastanime anilist notifier
|
||||
\b
|
||||
# with logging to stdout
|
||||
fastanime --log anilist notifier
|
||||
\b
|
||||
# with logging to a file. stored in the same place as your config
|
||||
fastanime --log-file anilist notifier
|
||||
"""
|
||||
@@ -1,18 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the top 15 most favourited anime from anilist",
|
||||
short_help="View most favourited anime",
|
||||
)
|
||||
@click.pass_obj
|
||||
def favourites(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_favourite()
|
||||
if anime_data[0]:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,61 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="Login to your anilist account")
|
||||
@click.option("--status", "-s", help="Whether you are logged in or not", is_flag=True)
|
||||
@click.option("--erase", "-e", help="Erase your login details", is_flag=True)
|
||||
@click.pass_obj
|
||||
def login(config: "Config", status, erase):
|
||||
from rich import print
|
||||
from rich.prompt import Confirm, Prompt
|
||||
|
||||
from ...utils.tools import exit_app
|
||||
|
||||
if status:
|
||||
is_logged_in = True if config.user else False
|
||||
message = (
|
||||
"You are logged in :smile:" if is_logged_in else "You arent logged in :cry:"
|
||||
)
|
||||
print(message)
|
||||
print(config.user)
|
||||
exit_app()
|
||||
elif erase:
|
||||
if Confirm.ask(
|
||||
"Are you sure you want to erase your login status", default=False
|
||||
):
|
||||
config.update_user({})
|
||||
print("Success")
|
||||
exit_app(0)
|
||||
else:
|
||||
exit_app(1)
|
||||
else:
|
||||
from click import launch
|
||||
|
||||
from ....anilist import AniList
|
||||
|
||||
if config.user:
|
||||
print("Already logged in :confused:")
|
||||
if not Confirm.ask("or would you like to reloggin", default=True):
|
||||
exit_app()
|
||||
# ---- new loggin -----
|
||||
print(
|
||||
f"A browser session will be opened ( [link]{config.fastanime_anilist_app_login_url}[/link] )",
|
||||
)
|
||||
launch(config.fastanime_anilist_app_login_url, wait=True)
|
||||
print("Please paste the token provided here")
|
||||
token = Prompt.ask("Enter token")
|
||||
user = AniList.login_user(token)
|
||||
if not user:
|
||||
print("Sth went wrong", user)
|
||||
exit_app()
|
||||
return
|
||||
user["token"] = token
|
||||
config.update_user(user)
|
||||
print("Successfully saved credentials")
|
||||
print(user)
|
||||
exit_app()
|
||||
@@ -1,124 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="Check for notifications on anime you currently watching")
|
||||
@click.pass_obj
|
||||
def notifier(config: "Config"):
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
from plyer import notification
|
||||
|
||||
from ....anilist import AniList
|
||||
from ....constants import APP_CACHE_DIR, APP_DATA_DIR, APP_NAME, ICON_PATH, PLATFORM
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
notified = os.path.join(APP_DATA_DIR, "last_notification.json")
|
||||
anime_image_path = os.path.join(APP_CACHE_DIR, "notification_image")
|
||||
notification_duration = config.notification_duration * 60
|
||||
notification_image_path = ""
|
||||
|
||||
if not config.user:
|
||||
print("Not Authenticated")
|
||||
print("Run the following to get started: fastanime anilist loggin")
|
||||
return
|
||||
run = True
|
||||
# WARNING: Mess around with this value at your own risk
|
||||
timeout = 2 # time is in minutes
|
||||
if os.path.exists(notified):
|
||||
with open(notified, "r") as f:
|
||||
past_notifications = json.load(f)
|
||||
else:
|
||||
past_notifications = {}
|
||||
with open(notified, "w") as f:
|
||||
json.dump(past_notifications, f)
|
||||
|
||||
while run:
|
||||
try:
|
||||
logger.info("checking for notifications")
|
||||
result = AniList.get_notification()
|
||||
if not result[0]:
|
||||
logger.warning(
|
||||
"Something went wrong this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
continue
|
||||
data = result[1]
|
||||
if not data:
|
||||
logger.warning(
|
||||
"Something went wrong this could mean anilist is down or you have lost internet connection"
|
||||
)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
continue
|
||||
|
||||
notifications = data["data"]["Page"]["notifications"]
|
||||
if not notifications:
|
||||
logger.info("Nothing to notify")
|
||||
else:
|
||||
for notification_ in notifications:
|
||||
anime_episode = notification_["episode"]
|
||||
anime_title = notification_["media"]["title"][
|
||||
config.preferred_language
|
||||
]
|
||||
title = f"{anime_title} Episode {anime_episode} just aired"
|
||||
# pyright:ignore
|
||||
message = "Be sure to watch so you are not left out of the loop."
|
||||
# message = str(textwrap.wrap(message, width=50))
|
||||
|
||||
id = notification_["media"]["id"]
|
||||
if past_notifications.get(str(id)) == notification_["episode"]:
|
||||
logger.info(
|
||||
f"skipping id={id} title={anime_title} episode={anime_episode} already notified"
|
||||
)
|
||||
|
||||
else:
|
||||
# windows only supports ico,
|
||||
# and you still ask why linux
|
||||
if PLATFORM != "Windows":
|
||||
image_link = notification_["media"]["coverImage"]["medium"]
|
||||
logger.info("Downloading image...")
|
||||
|
||||
resp = requests.get(image_link)
|
||||
if resp.status_code == 200:
|
||||
with open(anime_image_path, "wb") as f:
|
||||
f.write(resp.content)
|
||||
notification_image_path = anime_image_path
|
||||
else:
|
||||
logger.warn(
|
||||
f"Failed to get image response_status={resp.status_code} response_content={resp.content}"
|
||||
)
|
||||
notification_image_path = ICON_PATH
|
||||
else:
|
||||
notification_image_path = ICON_PATH
|
||||
|
||||
past_notifications[f"{id}"] = notification_["episode"]
|
||||
with open(notified, "w") as f:
|
||||
json.dump(past_notifications, f)
|
||||
logger.info(message)
|
||||
notification.notify( # pyright:ignore
|
||||
title=title,
|
||||
message=message,
|
||||
app_name=APP_NAME,
|
||||
app_icon=notification_image_path,
|
||||
hints={
|
||||
"image-path": notification_image_path,
|
||||
"desktop-entry": f"{APP_NAME}.desktop",
|
||||
},
|
||||
timeout=notification_duration,
|
||||
)
|
||||
time.sleep(30)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
logger.info("sleeping...")
|
||||
time.sleep(timeout * 60)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you paused on watching")
|
||||
@click.pass_obj
|
||||
def paused(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("PAUSED")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
anilist_config = FastAnimeRuntimeState()
|
||||
anilist_config.data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, anilist_config)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you are planning on watching")
|
||||
@click.pass_obj
|
||||
def planning(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("PLANNING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,17 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the top 15 most popular anime", short_help="View most popular anime"
|
||||
)
|
||||
@click.pass_obj
|
||||
def popular(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_popular()
|
||||
if anime_data[0]:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,27 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Get random anime from anilist based on a range of anilist anime ids that are seected at random",
|
||||
short_help="View random anime",
|
||||
)
|
||||
@click.pass_obj
|
||||
def random_anime(config):
|
||||
import random
|
||||
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
random_anime = range(1, 15000)
|
||||
|
||||
random_anime = random.sample(random_anime, k=50)
|
||||
|
||||
anime_data = AniList.search(id_in=list(random_anime))
|
||||
|
||||
if anime_data[0]:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
else:
|
||||
print(anime_data[1])
|
||||
@@ -1,18 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the 15 most recently updated anime from anilist that are currently releasing",
|
||||
short_help="View recently updated anime",
|
||||
)
|
||||
@click.pass_obj
|
||||
def recent(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_recently_updated()
|
||||
if anime_data[0]:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you are rewatching")
|
||||
@click.pass_obj
|
||||
def rewatching(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("REPEATING")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,17 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the 15 most scored anime", short_help="View most scored anime"
|
||||
)
|
||||
@click.pass_obj
|
||||
def scores(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
anime_data = AniList.get_most_scored()
|
||||
if anime_data[0]:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.data = anime_data[1]
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,21 +0,0 @@
|
||||
import click
|
||||
|
||||
from ...completion_functions import anime_titles_shell_complete
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Search for anime using anilists api and get top ~50 results",
|
||||
short_help="Search for anime",
|
||||
)
|
||||
@click.argument("title", shell_complete=anime_titles_shell_complete)
|
||||
@click.pass_obj
|
||||
def search(config, title):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, search_results = AniList.search(title)
|
||||
if success:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = search_results
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,18 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the top 15 anime that are currently trending",
|
||||
short_help="Trending anime 🔥🔥🔥",
|
||||
)
|
||||
@click.pass_obj
|
||||
def trending(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, data = AniList.get_trending()
|
||||
if success:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,17 +0,0 @@
|
||||
import click
|
||||
|
||||
|
||||
@click.command(
|
||||
help="Fetch the 15 most anticipited anime", short_help="View upcoming anime"
|
||||
)
|
||||
@click.pass_obj
|
||||
def upcoming(config):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces.anilist_interfaces import anilist_results_menu
|
||||
from ...utils.tools import FastAnimeRuntimeState
|
||||
|
||||
success, data = AniList.get_upcoming_anime()
|
||||
if success:
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = data
|
||||
anilist_results_menu(config, fastanime_runtime_state)
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import click
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...config import Config
|
||||
|
||||
|
||||
@click.command(help="View anime you are watching")
|
||||
@click.pass_obj
|
||||
def watching(config: "Config"):
|
||||
from ....anilist import AniList
|
||||
from ...interfaces import anilist_interfaces
|
||||
from ...utils.tools import FastAnimeRuntimeState, exit_app
|
||||
|
||||
if not config.user:
|
||||
print("Not authenticated")
|
||||
print("Please run: fastanime anilist loggin")
|
||||
exit_app()
|
||||
anime_list = AniList.get_anime_list("CURRENT")
|
||||
if not anime_list:
|
||||
return
|
||||
if not anime_list[0] or not anime_list[1]:
|
||||
return
|
||||
media = [
|
||||
mediaListItem["media"]
|
||||
for mediaListItem in anime_list[1]["data"]["Page"]["mediaList"]
|
||||
] # pyright:ignore
|
||||
anime_list[1]["data"]["Page"]["media"] = media # pyright:ignore
|
||||
fastanime_runtime_state = FastAnimeRuntimeState()
|
||||
fastanime_runtime_state.anilist_data = anime_list[1]
|
||||
anilist_interfaces.anilist_results_menu(config, fastanime_runtime_state)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user