├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── example
├── .env
├── README.MD
├── docker-compose-example.yml
└── docker-compose-home-example.yml
├── proxy
├── cloudflare-ips-conf.sh
└── proxy.conf
├── requirements.txt
└── tuberepair
├── api
├── __init__.py
├── channel.py
├── client_videos.py
├── playlist.py
├── static.py
└── video.py
├── config.py
├── config_env.py
├── main.py
├── modules
├── __init__.py
├── client
│ ├── __init__.py
│ ├── get.py
│ ├── helpers.py
│ └── logs.py
├── innertube
│ ├── client.py
│ ├── constants.py
│ ├── handler.py
│ └── parse
│ │ ├── __init__.py
│ │ └── featured.py
└── yt.py
├── static
└── categories.cat
└── templates
├── channel_info.jinja2
├── channel_playlists.jinja2
├── classic
├── featured.jinja2
└── search.jinja2
├── comments.jinja2
├── featured.jinja2
├── playlist_videos.jinja2
├── search_results.jinja2
├── search_results_channel.jinja2
├── uploads.jinja2
└── web
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | cache/
3 | .vscode/
4 | venv/
5 | .venv/
6 | .DS_Store
7 | .env
8 | *.txt
9 | test/
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginxproxy/nginx-proxy:alpine AS cf-proxy
2 |
3 | COPY ./proxy/cloudflare-ips-conf.sh ./proxy/proxy.conf ./
4 | RUN chmod +x ./proxy/cloudflare-ips-conf.sh && ./proxy/cloudflare-ips-conf.sh
5 | RUN mv allow-cf.conf ./proxy/proxy.conf /etc/nginx/conf.d/
6 |
7 |
8 | ######################
9 | # TUBEREPAIR #
10 | ######################
11 |
12 | FROM debian:stable
13 | ARG TUBEREPAIR_USER_UID="2000"
14 | ARG TUBEREPAIR_USER_GID="2000"
15 | EXPOSE 80
16 | EXPOSE 443
17 | LABEL NAME="TubeRepair tuberepair.uptimetrackers.com experimental"
18 | LABEL VERSION="0.1 Beta"
19 |
20 | COPY --chown=${TUBEREPAIR_USER_UID}:${TUBEREPAIR_USER_GID} ./requirements.txt /tuberepair-python/
21 |
22 | RUN /bin/bash | \
23 | groupadd -g ${TUBEREPAIR_USER_GID} tuberepair && \
24 | useradd tuberepair -u ${TUBEREPAIR_USER_UID} -g ${TUBEREPAIR_USER_GID} && \
25 | apt-get update && \
26 | apt-get install python3 python3-pip wget --no-install-recommends -y && \
27 | cd /tuberepair-python && \
28 | pip3 install -r requirements.txt --break-system-packages && \
29 | apt-get clean
30 |
31 | COPY --chown=${TUBEREPAIR_USER_UID}:${TUBEREPAIR_USER_GID} ./tuberepair /tuberepair-python
32 |
33 | WORKDIR /tuberepair-python
34 |
35 | USER tuberepair
36 |
37 | ENTRYPOINT ["python3", "main.py"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TubeRepair server, using Flask and Jinja2.
2 | - __Works out of the box, edit the backend to your likings.__
3 | - __Fetches from Youtube Private API without using a key__
4 | - Works with Classic YouTube, 1.0 to 2.2.0 for Google YouTube
5 | - ⚠️ This project will always be in beta. You can help in [bag's discord](https://discord.bag-xml.com) ⚠️
6 | - ⚠️ You can also fork this repo and create pull request if you'll like to add or fix things!⚠️
7 |
8 | # Features
9 | - Cache API responses
10 | - Customizable config
11 | - Docker compatible
12 | - Infinite scroling in search, channels, playlist, playlist videos, and comment
13 | - Allows users to select a video resolution in URL (example.com/360)
14 | - Supports sending request via proxie with Socks5 or Https (https://scrapfly.io/blog/python-requests-proxy-intro/)
15 |
16 | ### In the future
17 | - Based all requests via innertube (ditching invidious and request to youtube Private API directly)
18 | - Private server with password protection and secrets
19 |
20 | # Setting up
21 | Make sure you have Python (3.8 minimum) and virtualenv (optional) installed.
22 | ```bash
23 | # Download
24 | git clone https://github.com/kevinf100/tuberepair.uptimetrackers.com
25 | mv tuberepair.uptimetrackers.com/ tuberepairdocker/
26 | cd tuberepairdocker/tuberepair
27 |
28 | # Preparing virtualenv
29 | # You can just skip to pip, but for good measures.
30 | virtualenv tuberepair
31 | source tuberepair/bin/activate
32 | pip install -r requirements.txt
33 |
34 | # Running
35 | python main.py
36 | ```
37 |
38 | ## other notes
39 | Since YouTube is getting more aggressive with blockage, I suggest you host yourself a [Invidious](https://github.com/iv-org/invidious) instance, and set it via config.py. This makes sure that video will load (not metadata)...
40 |
41 | ...you could also use proxies to bypass this (as mentioned above), but that requires extensive "birdwatching" and a pain to deal with.
42 |
43 | # Docker
44 |
45 | Make sure you have Linux, ports 80 and 443 open, [Docker](https://docs.docker.com/engine/install/), and DNS record already pointing to your server.
46 |
47 | ```bash
48 | # Download
49 | git clone https://github.com/kevinf100/tuberepair.uptimetrackers.com
50 | mv tuberepair.uptimetrackers.com/ tuberepairdocker/
51 | cd tuberepairdocker
52 | cp ./example/.env ../
53 | cp ./example/docker-compose-example.yml ./example/docker-compose.yml
54 | ```
55 | ### docker-compose setup
56 | Next you'll need to edit the .env file or you can edit the docker-compose.yml directly.
57 | The .env in example is the bare minimum you need for the server to run.
58 | You can always add more to it.
59 |
60 |
61 | If you're NOT running Docker rootless
62 |
63 | Uncomment the lines in docker-compose.yml that say to uncomment it (Lines 38 and 56) and comment that lines above them (Lines 37 and 55).
64 | Comment the user USERID line (line 3) in .env.
65 |
66 |
67 |
68 |
69 | If you're running Docker rootless
70 |
71 | Get your userid
72 |
73 |
74 | ```
75 | bash
76 | id -u
77 | ```
78 | add it to USERID in the .env file.
79 |
80 |
81 |
82 | ### Start
83 | Once you edit the docker-compose.yml you are done and can run the server!
84 | ```bash
85 | # Start
86 | docker compose up -d --build
87 | ```
88 |
89 | # Credits
90 |
91 | ### Contributors
92 | - [kendoodoo](https://github.com/kendoodoo) (who started this)
93 | - [ObscureMosquito](https://github.com/ObscureMosquito) (YouTube Classic)
94 | - [Nishijima Akito](https://github.com/shijimasoft) (YouTube Classic)
95 | - [SpaceSaver](https://github.com/SpaceSaver) (YouTube Private API, HLS playback filter)
96 | - [Kevinf100](https://github.com/kevinf100) (Added features from the fork)
97 | - (et al.)
98 |
99 | ### Code
100 | I will not copy code that explicitly states "do not modify".
101 | - https://github.com/ftde0/yt2009
102 | __without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software__
103 | - https://github.com/tombulled/innertube
104 | __You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications__
105 |
--------------------------------------------------------------------------------
/example/.env:
--------------------------------------------------------------------------------
1 | HOST_URL=example.com
2 | LETSENCRYPT_EMAIL=your-email@example.com
3 | USERID={Can be found with 'id -u'. Remove this line if NOT ran in rootless}
--------------------------------------------------------------------------------
/example/README.MD:
--------------------------------------------------------------------------------
1 | # IF YOU ARE GOING TO USE THIS EXAMPLE COPY IT UP ONE DIRECTORY!
2 | ```
3 | cp .env docker-compose-example.yml ../
4 | ```
--------------------------------------------------------------------------------
/example/docker-compose-example.yml:
--------------------------------------------------------------------------------
1 | services:
2 | tuberepair:
3 | container_name: tuberepair
4 | restart: always
5 | build:
6 | context: .
7 | args:
8 | TUBEREPAIR_USER_UID: 2000
9 | TUBEREPAIR_USER_GID: 2000
10 | environment:
11 | - LETSENCRYPT_HOST=${HOST_URL}
12 | - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
13 | - VIRTUAL_HOST=${HOST_URL}
14 | - VIRTUAL_PORT=4000
15 | - SPYING=false
16 | - DEBUG=false
17 | - MEDIUM_QUALITY=true
18 | networks:
19 | - proxy-tier
20 | - default
21 | volumes:
22 | - /etc/localtime:/etc/localtime:ro
23 |
24 | proxy:
25 | build:
26 | context: .
27 | target: cf-proxy
28 | container_name: proxy
29 | restart: always
30 | environment:
31 | LOG_FORMAT: $$real_client_ip - $$remote_user [$$time_local] $$request $$status $$body_bytes_sent $$http_host $$upstream_response_time $$http_referer $$http_user_agent;
32 |
33 | volumes:
34 | - certs:/etc/nginx/certs:z,ro
35 | - vhost.d:/etc/nginx/vhost.d:z
36 | - html:/usr/share/nginx/html:z
37 | - /run/user/${USERID}/docker.sock:/tmp/docker.sock:z,ro # Being ran in rootless, comment or remove this line for docker being ran as root
38 | # - /var/run/docker.sock:/tmp/docker.sock:z,ro # Uncomment this line if your NOT running rootless docker
39 | - /etc/localtime:/etc/localtime:ro
40 | ports:
41 | - 80:80
42 | - 443:443
43 | labels:
44 | com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
45 |
46 | letsencrypt-companion:
47 | container_name: letsencrypt-companion
48 | image: nginxproxy/acme-companion
49 | restart: always
50 | volumes:
51 | - certs:/etc/nginx/certs:z
52 | - acme:/etc/acme.sh:z
53 | - vhost.d:/etc/nginx/vhost.d:z
54 | - html:/usr/share/nginx/html:z
55 | - /run/user/${USERID}/docker.sock:/var/run/docker.sock:z,ro # Being ran in rootless, comment or remove this line for docker being ran as root
56 | # - /var/run/docker.sock:/tmp/docker.sock:z,ro # Uncomment this line if your NOT running rootless docker
57 | - /etc/localtime:/etc/localtime:ro
58 | networks:
59 | - proxy-tier
60 | depends_on:
61 | - proxy
62 |
63 |
64 | volumes:
65 | certs:
66 | acme:
67 | vhost.d:
68 | html:
69 |
70 | networks:
71 | proxy-tier:
72 |
--------------------------------------------------------------------------------
/example/docker-compose-home-example.yml:
--------------------------------------------------------------------------------
1 | services:
2 | tuberepair:
3 | container_name: tuberepair
4 | restart: always
5 | build:
6 | context: .
7 | args:
8 | TUBEREPAIR_USER_UID: 2000
9 | TUBEREPAIR_USER_GID: 2000
10 | environment:
11 | - SPYING=false
12 | - DEBUG=false
13 | - MEDIUM_QUALITY=true
14 | ports:
15 | - 80:4000
16 | volumes:
17 | - /etc/localtime:/etc/localtime:ro
--------------------------------------------------------------------------------
/proxy/cloudflare-ips-conf.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | IPFILE=./ips-cloudflare.txt
3 | PROXYFILE=./proxy.conf
4 | ALLOWCF=./allow-cf.conf
5 | wget 'https://www.cloudflare.com/ips-v4/' -O $IPFILE
6 | printf "\n" >> $IPFILE
7 | wget 'https://www.cloudflare.com/ips-v6/' -O ->> $IPFILE
8 |
9 |
10 |
11 | for IP in $(cat $IPFILE); do
12 | # printf "set_real_ip_from $IP;\n" >> $PROXYFILE
13 | printf "allow $IP;\n" >> $ALLOWCF
14 | done
15 |
16 | #printf "\nreal_ip_header CF-Connecting-IP;" >> $PROXYFILE
17 | printf "\ndeny all;" >> $ALLOWCF
18 |
--------------------------------------------------------------------------------
/proxy/proxy.conf:
--------------------------------------------------------------------------------
1 | # Credits https://frankindev.com/2020/11/18/allow-cloudflare-only-in-nginx/
2 | map $http_x_forwarded_for $real_client_ip {
3 | ~^(\d+\.\d+\.\d+\.\d+) $1;
4 | default $http_cf_connecting_ip;
5 | }
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | Flask-CacheControl
3 | Flask-Compress
4 | Jinja2
5 | requests-cache
6 | requests
7 | waitress
8 | requests[socks]
9 | redis
--------------------------------------------------------------------------------
/tuberepair/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kendoodoo/tuberepair-python/49d7073be300e994fcca5bdebe9684dc191d5ea9/tuberepair/api/__init__.py
--------------------------------------------------------------------------------
/tuberepair/api/channel.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, Flask, request, redirect, render_template
2 | from functools import wraps
3 |
4 | from modules.client import get, helpers
5 | from modules import yt
6 | import config
7 | from modules.client.logs import print_with_seperator
8 |
9 | channel = Blueprint("channel", __name__)
10 |
11 | # get channel info
12 | @channel.route("/feeds/api/channels/")
13 | @channel.route("//feeds/api/channels/")
14 | def search(channel_id, res=''):
15 |
16 | url = request.url_root + str(res)
17 |
18 | # fetch from... you can't believe it.
19 | # TODO: Make this a config setting letting users use innertube or Invidious. NO.
20 | data = yt.simple_channel_info(channel_id)
21 | # Error handling
22 | if data and 'error' in data:
23 | return get.error()
24 |
25 | channel_url = data['channel_id']
26 | channel_name = data['name']
27 | channel_pic_url = data['profile_picture']
28 | sub_count = data['subscribers']
29 |
30 | return get.template('channel_info.jinja2',{
31 | 'author': channel_name,
32 | 'author_id': channel_url,
33 | 'channel_pic_url': channel_pic_url,
34 | 'subcount': sub_count,
35 | 'url': url
36 | })
37 |
38 | # search for channels
39 | @channel.route("/feeds/api/channels")
40 | @channel.route("//feeds/api/channels")
41 | def channels(res=''):
42 |
43 | url = request.url_root + str(res)
44 | query = request.args.get('q')
45 | current_page, next_page = helpers.process_start_index(request)
46 | data = get.fetch(f"{config.URL}/api/v1/search?q={query}&type=channel&page={current_page}")
47 |
48 | if not data:
49 | next_page = None
50 |
51 | # template
52 | return get.template('search_results_channel.jinja2',{
53 | 'data': data,
54 | 'url': url,
55 | 'next_page': next_page
56 | })
57 |
58 | #return get.error()
59 |
60 | @channel.route("/feeds/api/users//uploads")
61 | @channel.route("//feeds/api/users//uploads")
62 | def uploads(channel_id, res=''):
63 |
64 | # Clamp Res
65 | if type(kwargs.get('res', None)) == int:
66 | res = min(max(res, 144), config.RESMAX)
67 |
68 | url = request.url_root + str(res)
69 | continuation_token = request.args.get('continuation') and '&continuation=' + request.args.get('continuation') or ''
70 | # https://docs.invidious.io/api/channels_endpoint/#get-apiv1channelsidvideos
71 | # Despite documention says /latest takes in a continuation token, it doesn't
72 | # sort_by is broken according to documention and will default to newest
73 | # we will add it anyway incase it ever gets fixed
74 | data = get.fetch(f"{config.URL}/api/v1/channels/{channel_id}/videos?sort_by=newest{continuation_token}")
75 | # Templates have the / at the end, so let's remove it.
76 | if url[-1] == '/':
77 | url = url[:-1]
78 |
79 | if data:
80 | return get.template('uploads.jinja2',{
81 | 'data': data['videos'],
82 | 'unix': get.unix,
83 | 'continuation': 'continuation' in data and data['continuation'] or None,
84 | 'url': url
85 | })
86 |
87 | return get.error()
88 |
--------------------------------------------------------------------------------
/tuberepair/api/client_videos.py:
--------------------------------------------------------------------------------
1 | from modules import get, helpers
2 | from flask import Blueprint, Flask, request, redirect, render_template, Response
3 | import config
4 | from modules.client.logs import print_with_seperator
5 | from modules import yt
6 | import threading
7 | import time
8 |
9 | # TODO: what is this.
10 |
11 | client_videos = Blueprint("client_videos", __name__)
12 | videos_dict = {}
13 |
14 | def add_route(item):
15 | item_hash = hash(item)
16 | videos_dict[item_hash] = item
17 | return item_hash
18 |
19 | def delete_route(item_hash):
20 | time.sleep(15)
21 | videos_dict.pop(item_hash)
22 |
23 | @client_videos.route("/getURL", methods=['POST'])
24 | @client_videos.route("//getURL", methods=['POST'])
25 | def getURL(res=None):
26 | data = request.json
27 | if request.headers.get('HLS-Video'):
28 | url = yt.data_to_hls_url(data, res)
29 | item_hash = add_route(url)
30 | t = threading.Thread(target=delete_route, args=(item_hash,))
31 | t.start()
32 | return f'{request.root_url}/getURLFinal/{item_hash}'
33 | else:
34 | return yt.data_to_medium_url(data)
35 |
36 | @client_videos.route("/getURLFinal/")
37 | def getURLFinal(item_hash):
38 | item_hash = int(item_hash)
39 | return Response(videos_dict[item_hash], mimetype="application/vnd.apple.mpegurl")
--------------------------------------------------------------------------------
/tuberepair/api/playlist.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, g
2 | import config
3 | from modules.client import get, helpers
4 |
5 | playlist = Blueprint("playlist", __name__)
6 |
7 | # get playlists
8 | # TODO: get more video info since invidious simplified it.
9 | def playlists(channel_id, res=''):
10 |
11 | # Clamp Res
12 | if type(res) == int:
13 | res = min(max(res, 144), config.RESMAX)
14 |
15 | url = request.url_root + str(res)
16 | continuationToken = request.args.get('continuation') and '?continuation=' + request.args.get('continuation') or ''
17 | try:
18 | data = get.fetch(f"{config.URL}/api/v1/channels/{channel_id}/playlists{continuationToken}")
19 |
20 | if data:
21 | return get.template('channel_playlists.jinja2',{
22 | 'data': data['playlists'],
23 | 'continuation': 'continuation' in data and data['continuation'] or None,
24 | 'url': url,
25 | 'channel_id': channel_id
26 | })
27 | raise Exception("No Data was returned!")
28 | except:
29 | return get.error()
30 |
31 |
32 | # get playlist's video
33 | # TODO: fix the damn thing
34 | def playlists_video(playlist_id, res=''):
35 |
36 | max_results = request.args.get('max-results')
37 |
38 | # TODO: Find out what it wants when this happens.
39 | # This happens on YouTube 2.0.0, when you load a video from the playlist it add this
40 | # for the playlist queue
41 | if max_results and max_results == '0':
42 | return get.error()
43 | if playlist_id.strip().lower() == '(null)':
44 | return get.error()
45 |
46 | # Clamp Res
47 | if type(res) == int:
48 | res = min(max(res, 144), config.RESMAX)
49 |
50 | currentPage, next_page = helpers.process_start_index(request)
51 |
52 | query = f'page={currentPage}'
53 |
54 | # Santize and stitch
55 | query = query.replace('&', '&')
56 |
57 | url = request.url_root + str(res)
58 | data = get.fetch(f"{config.URL}/api/v1/playlists/{playlist_id}?{query}")
59 |
60 | if not data:
61 | next_page = None
62 |
63 | if data:
64 | return get.template('playlist_videos.jinja2',{
65 | 'data': data['videos'],
66 | 'unix': get.unix,
67 | 'url': url,
68 | 'next_page': next_page
69 | })
70 |
71 | return get.error()
72 |
73 | # Playlist search (v2.0.0)
74 | def playlists_search(res=''):
75 |
76 | # Clamp Res
77 | if type(res) == int:
78 | res = min(max(res, 144), config.RESMAX)
79 |
80 | search_keyword = request.args.get('q')
81 |
82 | if not search_keyword:
83 | return get.error()
84 |
85 | currentPage, next_page = helpers.process_start_index(request)
86 |
87 | # remove space character
88 | search_keyword = search_keyword.replace(" ", "%20")
89 |
90 | query = f'q={search_keyword}&type=playlist&page={currentPage}'
91 |
92 | # Santize and stitch
93 | query = query.replace('&', '&')
94 |
95 | url = request.url_root + str(res)
96 | data = get.fetch(f"{config.URL}/api/v1/search?{query}")
97 |
98 | if not data:
99 | next_page = None,
100 |
101 | return get.template('channel_playlists.jinja2',{
102 | 'data': data,
103 | 'url': url,
104 | 'next_page': next_page
105 | })
106 |
107 | # worse than hell, but works
108 | def assign(path, func):
109 | bprint = playlist
110 | # saves a ton of unnecessary
111 | bprint.add_url_rule(path, view_func=func)
112 | # here's your res, kevin
113 | bprint.add_url_rule("/" + path, view_func=func)
114 |
115 | assign("/feeds/api/users//playlists", playlists)
116 | assign("/feeds/api/playlists/", playlists_video)
117 | assign("/feeds/api/playlists/snippets", playlists_search)
118 |
--------------------------------------------------------------------------------
/tuberepair/api/static.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request, redirect, send_file, render_template, Response
2 | import config
3 | from uuid import uuid4
4 |
5 | static = Blueprint("static", __name__, static_folder="../static")
6 |
7 | # Still passed without unique keys.
8 | key = uuid4().hex
9 |
10 | # static contents
11 | # --------------------------------------------- #
12 |
13 | # WTF: kev, you're gonna put res on every corner of this thing?
14 | @static.route("/")
15 | def index():
16 | return render_template('web/index.html', version=config.VERSION, medium=config.MEDIUM_QUALITY, hls=config.HLS_RESOLUTION)
17 |
18 | # sidebar menu
19 | @static.route("/schemas/2007/categories.cat")
20 | @static.route("//schemas/2007/categories.cat")
21 | def sidebar(res=None):
22 | return static.send_static_file('categories.cat')
23 |
24 | # bypass login
25 | # for youtube classic
26 | @static.route("/youtube/accounts/applelogin1", methods=['POST'])
27 | @static.route("//youtube/accounts/applelogin1", methods=['POST'])
28 | def legacy_login_bypass(res=None):
29 | return f'''r2={key}\nhmackr2={key}'''
30 |
31 | # second layer
32 | # for youtube classic
33 | @static.route("/youtube/accounts/applelogin2", methods=['POST'])
34 | @static.route("//youtube/accounts/applelogin2", methods=['POST'])
35 | def legacy_login_bypass2(res=None):
36 | return f'''Auth={key}'''
37 |
38 | # --------------------------------------------- #
39 | # bypass login for Google YT
40 | @static.route("/youtube/accounts/registerDevice", methods=['POST'])
41 | @static.route("//youtube/accounts/registerDevice", methods=['POST'])
42 | def login_bypass(res=None):
43 | # return random key
44 | return f"DeviceId={key}\nDeviceKey={key}"
45 |
46 | # --------------------------------------------- #
47 | # feat: LOGIN. REAL FUCKING LOGIN.
48 | # note: PLEASE, PLEASE figure out.
49 |
50 | @static.route("/accounts/ClientLogin", methods=['POST'])
51 | def login_rel():
52 | return '''SID=DQAAAGgA...7Zg8CTN\nLSID=DQAAAGsA...lk8BBbG\nAuth=DQAAAGgA...dk3fA5N'''
--------------------------------------------------------------------------------
/tuberepair/api/video.py:
--------------------------------------------------------------------------------
1 | # imports
2 | from flask import Blueprint, Flask, request, redirect, render_template, Response, g
3 | from functools import wraps
4 |
5 | # custom ones
6 | from modules.client import get, helpers
7 | from modules.client.logs import print_with_seperator
8 | from modules import yt
9 | import config
10 |
11 | video = Blueprint("video", __name__)
12 |
13 | # added a flask decorator to make life easier
14 | def sanitize(f):
15 |
16 | @wraps(f)
17 | def log(*args, **kwargs):
18 |
19 | if type(kwargs.get('res', None)) == int:
20 | res = min(max(res, 144), config.RESMAX)
21 |
22 | return f(*args, **kwargs)
23 | return log
24 |
25 | # featured videos
26 | # 2 alternate routes for popular page and search results
27 | @sanitize
28 | def frontpage(regioncode="US", popular=None, res=''):
29 |
30 | url = request.url_root
31 |
32 | # Will be used for checking Classic
33 | user_agent = request.headers.get('User-Agent')
34 | print(user_agent)
35 |
36 | # print logs if enabled
37 | if config.SPYING == True:
38 | print_with_seperator("Region code: " + regioncode)
39 |
40 | if helpers.classic(user_agent):
41 | # get template
42 | return get.template('classic/featured.jinja2',{
43 | 'data': yt.trending_feeds(),
44 | 'unix': get.unix,
45 | 'url': url
46 | })
47 | else:
48 | # Google YT
49 | return get.template('featured.jinja2',{
50 | 'data': yt.trending_feeds(),
51 | 'unix': get.unix,
52 | 'url': url
53 | })
54 |
55 | return get.error()
56 |
57 | # search for videos
58 | # TODO: ditch.
59 | def search_videos(res=''):
60 |
61 | # Clamp Res
62 | if type(res) == int:
63 | res = min(max(res, 144), config.RESMAX)
64 |
65 | url = request.url_root + str(res)
66 | currentPage, next_page = helpers.process_start_index(request)
67 |
68 | user_agent = request.headers.get('User-Agent')
69 |
70 | search_keyword = request.args.get('q').replace(" ", "%20")
71 |
72 | if not search_keyword:
73 | return get.error()
74 |
75 | # print logs if enabled
76 | if config.SPYING == True:
77 | print_with_seperator('Searched: ' + search_keyword)
78 |
79 | # q and page is already made, so lets hand add it
80 | query = f'q={search_keyword}&type=video&page={currentPage}'
81 |
82 | # If we have orderby, turn it into invidious friendly parameters
83 | # Else ignore it
84 | orderby = request.args.get('orderby')
85 | if orderby in helpers.valid_search_orderby:
86 | query += f'&sort={helpers.valid_search_orderby[orderby]}'
87 |
88 | # If we have time, turn it into invidious friendly parameters
89 | # Else ignore it
90 | time = request.args.get('time')
91 | if time in helpers.valid_search_time:
92 | query += f'&date={helpers.valid_search_time[time]}'
93 |
94 | # If we have duration, turn it into invidious friendly parameters
95 | # Else ignore it
96 | duration = request.args.get('duration')
97 | if duration in helpers.valid_search_duration:
98 | query += f'&duration={helpers.valid_search_duration[duration]}'
99 |
100 | # If we have captions, turn it into invidious friendly parameters
101 | # Else ignore it
102 | # NOTE: YouTube 1.1.0 app only supports subtitles in the search
103 | caption = request.args.get('caption')
104 | if type(caption) == str and caption.lower() == 'true':
105 | query += '&features=subtitles'
106 |
107 | # Santize and stitch
108 | query = query.replace('&', '&')
109 |
110 | # search by videos
111 | data = get.fetch(f"{config.URL}/api/v1/search?{query}")
112 |
113 | if not data:
114 | next_page = None
115 |
116 | # classic tube check
117 | if helpers.classic(user_agent):
118 | return get.template('classic/search.jinja2',{
119 | 'data': data,
120 | 'unix': get.unix,
121 | 'url': url,
122 | 'next_page': next_page
123 | })
124 | else:
125 | return get.template('search_results.jinja2',{
126 | 'data': data,
127 | 'unix': get.unix,
128 | 'url': url,
129 | 'next_page': next_page
130 | })
131 |
132 | # video's comments
133 | # IDEA: filter the comments too?
134 | def comments(videoid, res=''):
135 |
136 | # Clamp Res
137 | if type(res) == int:
138 | res = min(max(res, 144), config.RESMAX)
139 |
140 | url = request.url_root + str(res)
141 |
142 | # this shit is so messy, ditchchhhhhh
143 | continuation_token = request.args.get('continuation') and '&continuation=' + request.args.get('continuation') or ''
144 | # fetch invidious comments api
145 | data = get.fetch(f"{config.URL}/api/v1/comments/{videoid}?sortby={config.SORT_COMMENTS}{continuation_token}")
146 |
147 | if data:
148 | # NOTE: No comments sometimes returns {'error': 'Comments not found.'}
149 | if 'error' in data:
150 | comments = None
151 | else:
152 | comments = data['comments']
153 | return get.template('comments.jinja2',{
154 | 'data': comments,
155 | 'unix': get.unix,
156 | 'url': url,
157 | 'continuation': 'continuation' in data and data['continuation'] or None,
158 | 'video_id': videoid
159 | })
160 |
161 | return get.error()
162 |
163 | if (config.USE_INNERTUBE):
164 | # fetches video from innertube.
165 | @video.route("/getvideo/")
166 | @video.route("//getvideo/")
167 | def getvideo(video_id, res=None):
168 | if res is not None or config.MEDIUM_QUALITY is False:
169 |
170 | # Clamp Res
171 | if type(res) == int:
172 | res = min(max(res, 144), config.RESMAX)
173 |
174 | # Set mimetype since videole device don't recognized it.
175 | return Response(yt.video.hls_video_url(video_id, res), mimetype="application/vnd.apple.mpegurl")
176 |
177 | # 360p if enabled
178 | return redirect(yt.video.medium_quality_video_url(video_id), 307)
179 | else:
180 | # fetches video from invidious.
181 | @video.route("/getvideo/")
182 | @video.route("//getvideo/")
183 | def getvideo(video_id, res = None):
184 | data = get.fetch(f"{config.URL}/api/v1/videos/{video_id}")
185 | '''
186 | if res is not None or config.MEDIUM_QUALITY is False:
187 |
188 | # Clamp Res
189 | if type(res) == int:
190 | res = min(max(res, 144), config.RESMAX)
191 |
192 | for adaptive in data['adaptiveFormats']:
193 |
194 | return Response(yt.hls_video_url(video_id, res), mimetype="application/vnd.apple.mpegurl")
195 | '''
196 | # 360p if enabled
197 | # TODO: Fix resoution not working.
198 | return redirect(data['formatStreams'][0]['url'], 307)
199 |
200 | def get_suggested(video_id, res=''):
201 |
202 | data = get.fetch(f"{config.URL}/api/v1/videos/{video_id}")
203 |
204 | url = request.url_root + str(res)
205 | user_agent = request.headers.get('User-Agent')
206 |
207 | if data:
208 | if 'error' in data:
209 | data = None
210 | else:
211 | data = data['recommendedVideos']
212 | # classic tube check
213 | if helpers.classic(user_agent):
214 | return get.template('classic/search.jinja2',{
215 | 'data': data,
216 | 'unix': get.unix,
217 | 'url': url,
218 | 'next_page': None
219 | })
220 |
221 | return get.template('search_results.jinja2',{
222 | 'data': data,
223 | 'unix': get.unix,
224 | 'url': url,
225 | 'next_page': None
226 | })
227 | return get.error()
228 |
229 | # worse than hell, but it works
230 | def assign(path, func):
231 | bprint = video
232 | # saves a ton of unnecessary
233 | bprint.add_url_rule(path, view_func=func)
234 |
235 | # here's your res, kevin
236 | bprint.add_url_rule("/" + path, view_func=func)
237 |
238 | # paths for trending feeds
239 | assign("/feeds/api/standardfeeds//", frontpage)
240 | assign("/feeds/api/standardfeeds/", frontpage)
241 | # paths for search videos
242 | assign("/feeds/api/videos/", search_videos)
243 | # paths for video comments
244 | assign("/api/videos//comments", comments)
245 | assign("/feeds/api/videos//comments", comments)
246 | # get video
247 | assign("/getvideo/", getvideo)
248 | # get related videos
249 | assign("/feeds/api/videos//related", get_suggested)
--------------------------------------------------------------------------------
/tuberepair/config.py:
--------------------------------------------------------------------------------
1 | # incase you used one of those files.
2 | import config_env
3 | # Redis
4 | from requests_cache import RedisCache
5 |
6 | # -- DEV ZONE -- #
7 | # You can change this to anything.
8 | VERSION = "v0.0.9-beta"
9 | # -------------- #
10 |
11 | # TODO: make this less of a mess.
12 |
13 | # -- General -- #
14 |
15 | # gets 360p by default is user doesn't provide a resolution
16 | # NOTE: loads a ton faster
17 | MEDIUM_QUALITY = True
18 |
19 | # Use innertube
20 | # Directly the YouTube Private API!
21 | USE_INNERTUBE = True
22 |
23 | # resolution for DEFAULT HLS playback
24 | # None, 144, 240, 360, 480, 720, 1080...
25 | HLS_RESOLUTION = 720
26 |
27 | # Use invidious
28 | # NOTE: for video fetching, in case of emergency.
29 | # add http:// or https://
30 | URL = "https://id.420129.xyz"
31 |
32 | # Spying on stuff
33 | # NOTE: Don't judge people on their search lol
34 | SPYING = True
35 |
36 | # Set amount of featured videos.
37 | # NOTE: to cut bandwidth or load, either way.
38 | FEATURED_VIDEOS = 15
39 |
40 | # Set amount of comments
41 | # For each continuation.
42 | COMMENTS = 20
43 |
44 | # Sort comments by...
45 | # "newest", "popular"
46 | SORT_COMMENTS = "popular"
47 |
48 | # -- Misc -- #
49 |
50 | # Set port
51 | # Anything around 1 to 65000-ish
52 | # NOTE: set common ports so you can remember it. like 3000, 4000, 8000...
53 | PORT = 2000
54 |
55 | # Debug mode
56 | # NOTE: ALWAYS turn this on if you want to report bugs.
57 | DEBUG = True
58 |
59 | # TODO: explain me this too. what is this?
60 | CLIENT_TEST = False
61 |
62 | # Compress XML responses by GZIP
63 | # NOTE: Not resource intensive.
64 | COMPRESS = True
65 |
66 | # Set maximum resolution to use in URL.
67 | # TODO: explain me this.
68 | RESMAX = 36000
69 |
70 | # TODO: overlapping "SPYING"???
71 | GET_ERROR_LOGGING = True
72 |
73 | # ---------- #
74 |
75 | # If you know what you're doing, go ahead.
76 | # (so cliche...)
77 |
78 | # -- database -- #
79 |
80 | # cache for info
81 | # (in hours)
82 | CACHE_INFO = 1
83 |
84 | # cache for videos
85 | # NOTE: google will automatically remove the link in 5 hours
86 | # (in hours)
87 | CACHE_VIDEO = 4
88 |
89 | # Use REDIS? NO THANKS.
90 | # You could use it tho...
91 | USE_REDIS = False
92 |
93 | # TODO: put it somewhere else...
94 | if USE_REDIS:
95 | backend = RedisCache(host=REDIS_HOST, port=REDIS_PORT)
96 |
97 | # TODO: explain me this. what is this?
98 | backend = 'sqlite'
99 |
100 | # --------- #
--------------------------------------------------------------------------------
/tuberepair/config_env.py:
--------------------------------------------------------------------------------
1 | import os
2 | from requests_cache import RedisCache
3 | from modules.client import helpers
4 |
5 | OSEnv = os.environ
6 |
7 | def string_to_bool(input):
8 | if not isinstance(input, str):
9 | raise ValueError("A String was not passed")
10 | lower_input = input.lower()
11 | if lower_input == "true":
12 | return True
13 | elif lower_input == "false":
14 | return False
15 | raise ValueError("This string isn't true of false!")
16 |
17 | if "USE_REDIS" in OSEnv:
18 | USE_REDIS = string_to_bool(OSEnv["USE_REDIS"])
19 | backend = RedisCache(host=OSEnv["REDIS_HOST"], port=OSEnv["REDIS_PORT"])
20 |
21 | if "USE_INNERTUBE" in OSEnv:
22 | USE_INNERTUBE = string_to_bool(OSEnv["USE_INNERTUBE"])
23 |
24 | if "CLIENT_TEST" in OSEnv:
25 | CLIENT_TEST = string_to_bool(OSEnv["CLIENT_TEST"])
26 |
27 | if "MEDIUM_QUALITY" in OSEnv:
28 | MEDIUM_QUALITY = string_to_bool(OSEnv["MEDIUM_QUALITY"])
29 |
30 | if "GET_ERROR_LOGGING" in OSEnv:
31 | GET_ERROR_LOGGING = string_to_bool(OSEnv["GET_ERROR_LOGGING"])
32 |
33 | if "HLS_RESOLUTION" in OSEnv:
34 | HLS_RESOLUTION = int(OSEnv["HLS_RESOLUTION"])
35 |
36 | if "URL" in OSEnv:
37 | URL = OSEnv["URL"]
38 |
39 | if "PROXY" in OSEnv:
40 | helpers.setup_proxies(OSEnv["PROXY"])
41 |
42 | if "RESMAX" in OSEnv and OSEnv["RESMAX"].isdigit():
43 | RESMAX = int(OSEnv["RESMAX"])
44 |
45 | if "PORT" in OSEnv:
46 | PORT = OSEnv["PORT"]
47 |
48 | if "DEBUG" in OSEnv:
49 | DEBUG = string_to_bool(OSEnv["DEBUG"])
50 |
51 | if "SPYING" in OSEnv:
52 | SPYING = string_to_bool(OSEnv["SPYING"])
53 |
54 | if "COMPRESS" in OSEnv:
55 | COMPRESS = string_to_bool(OSEnv["COMPRESS"])
56 |
57 | if "FEATURED_VIDEOS" in OSEnv:
58 | FEATURED_VIDEOS = min(int(OSEnv["FEATURED_VIDEOS"]), 50)
59 |
60 | if "COMMENTS" in OSEnv:
61 | COMMENTS = min(int(OSEnv["COMMENTS"]), 20)
62 |
63 | if "SORT_COMMENTS" in OSEnv:
64 | SORT_COMMENTS = OSEnv["SORT_COMMENTS"]
65 |
--------------------------------------------------------------------------------
/tuberepair/main.py:
--------------------------------------------------------------------------------
1 | # Version whatever...
2 | import signal
3 | from flask import Flask, g
4 | from flask_compress import Compress
5 | import config
6 | from waitress import serve
7 |
8 | # seperated apis
9 | from api.static import static
10 | from api.playlist import playlist
11 | from api.video import video
12 | from api.channel import channel
13 |
14 | # log
15 | from modules.client import logs
16 |
17 | if config.CLIENT_TEST:
18 | from api.client_videos import client_videos
19 |
20 | # init
21 | # load version text
22 | logs.version(config.VERSION)
23 | app = Flask(__name__)
24 |
25 | # HACK: remove the slash at the end. GLOBALLY.
26 | # checkmate, kevin!
27 | app.url_map.strict_slashes = False
28 |
29 | # trim jinja2 whitespace
30 | # just a little fine tuning
31 | app.jinja_env.trim_blocks = True
32 | app.jinja_env.lstrip_blocks = True
33 |
34 | # register seperate paths
35 | app.register_blueprint(static)
36 | app.register_blueprint(playlist)
37 | app.register_blueprint(video)
38 | app.register_blueprint(channel)
39 | if config.CLIENT_TEST:
40 | app.register_blueprint(client_videos)
41 |
42 | # use compression to load faster via client
43 | if config.COMPRESS:
44 | compress = Compress(app)
45 |
46 | # config
47 | if __name__ == "__main__":
48 | # Docker shenanigans
49 | signal.signal(signal.SIGTERM, lambda *args: exit())
50 |
51 | # TODO: what's the point of disabling DEBUG?
52 | if config.DEBUG:
53 | app.run(port=config.PORT, host="0.0.0.0", debug=True)
54 | else:
55 | serve(app, port=config.PORT, host="0.0.0.0")
--------------------------------------------------------------------------------
/tuberepair/modules/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kendoodoo/tuberepair-python/49d7073be300e994fcca5bdebe9684dc191d5ea9/tuberepair/modules/__init__.py
--------------------------------------------------------------------------------
/tuberepair/modules/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kendoodoo/tuberepair-python/49d7073be300e994fcca5bdebe9684dc191d5ea9/tuberepair/modules/client/__init__.py
--------------------------------------------------------------------------------
/tuberepair/modules/client/get.py:
--------------------------------------------------------------------------------
1 | from requests_cache import CachedSession
2 | from jinja2 import Environment, FileSystemLoader
3 | from flask import request
4 | from datetime import timedelta, datetime
5 | import requests
6 | import json
7 |
8 | # custom functions
9 | import config
10 | from . import helpers
11 | from modules.client.logs import print_with_seperator
12 |
13 | # cache yt's (or invidious's) video request
14 | # mostly infos and links
15 | info_cache = CachedSession('cache/info', expire_after=timedelta(hours=config.CACHE_INFO), backend=config.backend)
16 |
17 | # cache to not spam the invidious instance
18 | session = info_cache
19 |
20 | def unix(unix="0"):
21 | return datetime.fromtimestamp(int(unix)).isoformat() + '.000Z'
22 |
23 | # Will be used in another update, but WHEN??? HUH???
24 | def unix_now():
25 | return datetime.now().isoformat() + '.000Z'
26 |
27 | # jinja2 path
28 | env = Environment(loader=FileSystemLoader('templates'))
29 |
30 | # simplify requests
31 | def fetch(url):
32 | try:
33 | # Without sending User-Agent, the instance wouldn't send any data for {url}/api/v1/videos ... Why?
34 | # EXPLAINATION: prevent bots from scraping other bots.
35 | url = session.get(url, headers={'User-Agent': 'TubeRepair'}, proxies=helpers.proxies)
36 | data = url.json()
37 | return data
38 | except requests.ConnectionError:
39 | print_with_seperator('INVIDIOUS INSTANCE FAILED!', 'red')
40 |
41 | # If error logging is enable, detour the function.
42 | if config.GET_ERROR_LOGGING:
43 | orig_fetch = fetch
44 | def fetch(url):
45 | data = orig_fetch(url)
46 | #print_with_seperator(data)
47 | if not data:
48 | print_with_seperator(f'request "{url}" returned nothing from instance.')
49 | elif 'error' in data:
50 | print_with_seperator(f'Invidious returned an error processing "{url}"\n----Error begins below----\n{data}\n----End of error----')
51 | return data
52 |
53 | # read template from jinja2
54 | def template(file, render_data):
55 | t = env.get_template(file)
56 | output = t.render(render_data)
57 | return output
58 |
59 | # YT app will turn up nothing if 404 was passed.
60 | def error():
61 | return "", 404
--------------------------------------------------------------------------------
/tuberepair/modules/client/helpers.py:
--------------------------------------------------------------------------------
1 | # helpers - misc stuff, mostly for apis
2 |
3 | import werkzeug
4 | from .logs import print_with_seperator
5 |
6 | # TODO: comment how this works?
7 |
8 | valid_search_orderby = {
9 | 'relevance': 'relevance',
10 | 'published': 'date',
11 | 'viewCount': 'views',
12 | 'rating': 'rating'
13 | }
14 |
15 | valid_search_time = {
16 | 'today': 'today',
17 | 'this_week': 'week',
18 | 'this_month': 'month'
19 | }
20 |
21 | valid_search_duration = {
22 | 'short': 'short',
23 | 'long': 'long'
24 | }
25 |
26 | # user agent check
27 | def classic(string):
28 | # should it be something like "sanitize"?
29 | processed = string.lower()
30 | if "youtube/1.0.0" in processed or "youtube v1.0.0" in processed:
31 | return True
32 | else:
33 | return False
34 |
35 | # proxy zone
36 | proxies = None
37 |
38 | def setup_proxies(proxy):
39 | global proxies
40 | proxies = {
41 | "http": proxy,
42 | "https": proxy
43 | }
44 |
45 | def process_start_index(request):
46 | if type(request) is not werkzeug.local.LocalProxy:
47 | raise ValueError("request SHOULD BE werkzeug.local.LocalProxy! SOMETHING IS WRONG!")
48 |
49 | # Getting current url with all the query info
50 | next_page = request.url
51 | # Get 'start-index' query for later use
52 | start_index = request.args.get('start-index')
53 | # Get current page or start at the first page if 'start-index' is missing or invalid
54 | if start_index and start_index.isdigit():
55 | current_page = start_index
56 | else:
57 | current_page = '1'
58 | # Setup for next page
59 | next_pageNumber = int(current_page) + 1
60 | # Checks if we have a 'start-index'
61 | if start_index:
62 | # Replace for next page
63 | next_page = next_page.replace(f'start-index={current_page}', f'start-index={next_pageNumber}')
64 | else:
65 | # Add query for next page
66 | next_page += f'&start-index={next_pageNumber}'
67 | # Santize
68 | next_page = next_page.replace('&', '&')
69 |
70 | return current_page, next_page
--------------------------------------------------------------------------------
/tuberepair/modules/client/logs.py:
--------------------------------------------------------------------------------
1 | # Colors https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
2 | class color:
3 | GREEN = '\033[92m'
4 | YELLOW = '\033[93m'
5 | RED = '\033[91m'
6 | DEFAULT = '\033[39m'
7 |
8 | # Things that aren't colors.
9 | # TODO: prove you wrong.
10 | class other_chars:
11 | END = '\033[0m'
12 | BOLD = '\033[1m'
13 | HEADER = '\033[95m'
14 |
15 | # Dictonary to turn string into color at o(1) time. Useful if we ever add more colors.
16 | color_picker = {
17 | 'green': color.GREEN,
18 | 'yellow': color.YELLOW,
19 | 'red': color.RED,
20 | }
21 |
22 | # Used to turn a string into a color
23 | def str_to_color(color_string):
24 | # All lower case for comparing
25 | color_string = color_string.lower()
26 | # Check if we have a code for the color
27 | if color_string not in color_picker:
28 | return color.DEFAULT
29 | # Return the color
30 | return color_picker[color_string]
31 |
32 | # Used to generate seperators
33 | def seperator(length):
34 | return "-" * length
35 |
36 | # print out the version
37 | def version(version):
38 | print('\n' + color.GREEN + 'TubeRepair server ' + version + other_chars.END)
39 | # just, forgive me.
40 | print(seperator(len('TubeRepair server ' + version)) + '\n')
41 |
42 | # Helps seperator our string for logging
43 | def print_with_seperator(string, colors='green'):
44 | string = str(string)
45 | # Begining seperator
46 | print(seperator(20))
47 | # Get our color
48 | use_color = str_to_color(colors)
49 | # Print out using our color
50 | print(use_color + string + other_chars.END)
51 | # End seperator
52 | print(seperator(20))
--------------------------------------------------------------------------------
/tuberepair/modules/innertube/client.py:
--------------------------------------------------------------------------------
1 | from requests_cache import CachedSession
2 | from datetime import timedelta
3 |
4 | from . import constants
5 | from modules.client import helpers
6 | import config
7 |
8 | # cache videos.
9 | video_cache = CachedSession('cache/videos', expire_after=timedelta(hours=config.CACHE_VIDEO), ignored_parameters=['key'], allowable_methods=['POST'], backend=config.backend)
10 | # Video expires after 5 hours
11 | session = video_cache
12 |
13 | # TODO: add user agent spoofing. it might not work, but worth a shot.
14 | # gl = region (example: US, UK, VN)
15 | # Usage: Client(["WEB", "version"], "US")
16 | def Client(config, gl="US"):
17 |
18 | mock = {"client": {
19 | "hl": "en",
20 | "gl": gl,
21 | "clientName": config[0],
22 | "clientVersion": config[1]
23 | }}
24 |
25 | return mock
26 |
27 | # fetch the innertube API.
28 | def post(url, json):
29 | data = session.post(url, json=json, proxies=helpers.proxies).json()
30 | return data
31 |
32 | # fetch the innertube API.
33 | def get(url, json):
34 | data = session.get(url, json=json, proxies=helpers.proxies).json()
35 | return data
--------------------------------------------------------------------------------
/tuberepair/modules/innertube/constants.py:
--------------------------------------------------------------------------------
1 | # Yes, I copied this from youtubei.js. CONSTANTS 4ever.
2 | # Kind of shitty.
3 |
4 | base_url = 'https://www.youtube.com/youtubei/v1/'
5 |
6 | # hard-coded API Key, from youtube's private API
7 | # no one bothered to change it. works with every client.
8 | key = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
9 |
10 | player = base_url + 'player?key=' + key
11 | browse = base_url + 'browse?key=' + key
12 | search = base_url + 'search?key=' + key
13 |
14 | class client:
15 | WEB = ["WEB", "2.20230728.00.00"]
16 | WEB_KIDS = ["WEB_KIDS", "2.20230111.00.00"]
17 | IOS = ["IOS", "19.16.3"]
18 | ANDROID = ["ANDROID", "19.17.34"]
19 |
20 | # params: a "nice" way to spot trending topics by ID.
21 | # google, i hate you.
22 | class param:
23 | def video(type):
24 | if type == "music":
25 | return "4gINGgt5dG1hX2NoYXJ0cw%3D%3D"
26 | if type == "gaming":
27 | return "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D"
28 | if type == "movie":
29 | return "4gIKGgh0cmFpbGVycw%3D%3D"
30 |
31 | def search(type):
32 | if type == "videos":
33 | return "EgIQAQ%3D%3D"
34 | if type == "playlists":
35 | return "CAASAhAD"
36 | if type == "channels":
37 | return "CAASAhAC"
38 | if type == "movies":
39 | return "CAASAhAE"
40 |
41 | channel_info = "EgZzaG9ydHPyBgUKA5oBAA%3D%3D"
42 |
43 | # I'll probably put more, but the ID format sucks
44 | # MANUALLY.
--------------------------------------------------------------------------------
/tuberepair/modules/innertube/handler.py:
--------------------------------------------------------------------------------
1 | # Handle innertube's format (in general)
2 | from datetime import timedelta, datetime
3 |
4 | # convert subscriber to text
5 | # example: 10.1M (as string) -> 10100000 (as int)
6 | def subscribers(string):
7 | processed_string = string.replace('subscribers', '')
8 |
9 | if 'M' in processed_string:
10 | return int(float(processed_string.replace('M', '')) * 100000.0)
11 |
12 | if 'K' in processed_string:
13 | return int(float(processed_string.replace('K', '')) * 1000.0)
14 |
15 | else:
16 | return processed_string
17 |
18 | # convert views to text
19 | # example: '123,498 views' (as string) -> 123498 (as int)
20 | def views(string):
21 | processed_string = str(string).replace(' views', '').replace(',', '')
22 | return int(processed_string)
23 |
24 | # convert yt timestamp to seconds (thanks stackoverflow)
25 | # example 40:56 -> in seconds (as int)
26 | def to_seconds(timestr):
27 | seconds = 0
28 | for part in timestr.split(':'):
29 | seconds = seconds * 60 + int(part, 10)
30 | return seconds
31 |
32 | # convert annoying time string
33 | # TODO: do we need to get the exact timezone?
34 | # '1 hours ago' -> unix
35 | def date_converter(string):
36 |
37 | # ['1', 'hours', 'ago']
38 | split_string = string.split(' ')
39 | current_time = datetime.now()
40 |
41 | # I have to do this manually because python librarys are not perfect.
42 | if split_string[1] == "minutes":
43 | return current_time - timedelta(minutes=int(split_string[0]))
44 | if split_string[1] == "hours":
45 | return current_time - timedelta(hours=int(split_string[0]))
46 | if split_string[1] == "days":
47 | return current_time - timedelta(days=int(split_string[0]))
48 | if split_string[1] == "months":
49 | return current_time - timedelta(weeks=int(split_string[0] * 4))
50 | if split_string[1] == "years":
51 | return current_time - timedelta(days=int(split_string[0]) * 365)
52 |
53 | class misc:
54 |
55 | # Thanks to spacesaver.
56 | def hls_quality_split(hls, res=None):
57 | panda = hls.split("\n")
58 | # regex filter
59 | formatfilter = re.compile(r"^#EXT-X-STREAM-INF:BANDWIDTH=(?P\d+),CODECS=\"(?P[^\"]+)\",RESOLUTION=(?P\d+)x(?P\d+),FRAME-RATE=(?P\d+),VIDEO-RANGE=(?P[^,]+),AUDIO=\"(?P[^\"]+)\"(,SUBTITLES=\"(?P[^\"]+)\")?")
60 | vertical = None
61 | maxRes = 0
62 | wanted_resolution = res and type(res) == int and min(max(res, 144), config.RESMAX) or config.HLS_RESOLUTION or 360
63 | # doesn't bother to explain the code, sooo...
64 | # TODO: explain the thing, lazer eyed cat.
65 | for x in range(len(panda)):
66 | line = panda[x]
67 | match = formatfilter.match(line)
68 |
69 | # dude
70 | if not match:
71 | continue
72 |
73 | # continue if codecs is not compatible (or matched?)
74 | if not match.group("codecs").startswith("avc"):
75 | panda[x] = ""
76 | panda[x+1] = ""
77 | continue
78 |
79 | # reject framerates over 30
80 | if int(match.group("fps")) > 30:
81 | panda[x] = ""
82 | panda[x+1] = ""
83 | continue
84 |
85 | if vertical is None:
86 | vertical = int(match.group("height")) > int(match.group("width"))
87 | res = 0
88 |
89 | # match vertical with width, because higher
90 | if vertical:
91 | res = int(match.group("width"))
92 | else:
93 | res = int(match.group("height"))
94 |
95 | # if resolution bigger than the expected height, skip
96 | if res > wanted_resolution:
97 | panda[x] = ""
98 | panda[x+1] = ""
99 | continue
100 |
101 | if res > maxRes:
102 | maxRes = res
103 |
104 | for x in range(len(panda)):
105 | line = panda[x]
106 | match = formatfilter.match(line)
107 |
108 | if not match:
109 | continue
110 | res = 0
111 |
112 | if vertical:
113 | res = int(match.group("width"))
114 | else:
115 | res = int(match.group("height"))
116 |
117 | if res < maxRes:
118 | panda[x] = ""
119 | panda[x+1] = ""
120 |
121 | panda = "\n".join(panda)
122 | return panda
--------------------------------------------------------------------------------
/tuberepair/modules/innertube/parse/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kendoodoo/tuberepair-python/49d7073be300e994fcca5bdebe9684dc191d5ea9/tuberepair/modules/innertube/parse/__init__.py
--------------------------------------------------------------------------------
/tuberepair/modules/innertube/parse/featured.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | def trending_feeds(region="US", type=""):
4 |
5 | resp_json = []
6 |
7 | json_data = {
8 | "browseId": "FEtrending",
9 | "context": Client("WEB", "2.20230728.00.00", region)
10 | }
11 |
12 | # SEND TO YOUTUBE IMMEDIATELY
13 | data = session.post(client.browse, json=json_data).json()
14 | init = data['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']
15 |
16 | # contents at item #3 (3 starting from 0), is intentional
17 | # 0 = 'is sponsored?'
18 | # 1 = '2 random videos'
19 | # 2 = 'shorts'
20 | video_items = init['contents'][3]['itemSectionRenderer']['contents'][0]['shelfRenderer']['content']['expandedShelfContentsRenderer']['items']
21 |
22 | # TODO: add a invidious fallback for certain cases of failure
23 | for x in video_items:
24 | video = x['videoRenderer']
25 | # return in dict
26 | resp_json.append(dict(
27 |
28 | video_id=video['videoId'],
29 | channel_id=video['ownerText']['runs'][0]['navigationEndpoint']['browseEndpoint']['browseId'],
30 | channel_name=video['ownerText']['runs'][0]['text'],
31 | title=video['title']['runs'][0]['text'],
32 | # hottest fix on earth.
33 | # TODO: put KeyError to jail.
34 | description=(video['descriptionSnippet']['runs'][0]['text'] if 'descriptionSnippet' in video else ''),
35 | views=handler.views(video['viewCountText']['simpleText'] if 'viewCountText' in video else 0),
36 | length=handler.to_seconds(video['lengthText']['simpleText'])
37 |
38 | ))
39 |
40 | return resp_json
--------------------------------------------------------------------------------
/tuberepair/modules/yt.py:
--------------------------------------------------------------------------------
1 | # NOTE: check them first
2 | from .innertube import client, handler, constants
3 | from .innertube.client import Client
4 |
5 | # TBH, I don't even know how to use python.
6 | class video:
7 | # Get HLS URL via innertube and fetch the file, then filter to fix auto quality playback error
8 | # SpaceSaver.
9 | def data_to_hls_url(data, res=None):
10 | # get video's m3u8 to process it.
11 | stream = data["streamingData"]["hlsManifestUrl"]
12 | data = client.get(stream, proxies=helpers.proxies).text
13 |
14 | # spliting to... split the code for readability.
15 | return handler.misc.hls_quality_split(data, res)
16 |
17 | def hls_video_url(video_id, res=None):
18 | # using IOS client since Apple invented HLS.
19 | json_data = {
20 | "videoId": video_id,
21 | "context": Client(constants.client.IOS)
22 | }
23 |
24 | # fetch innertube
25 | data = client.post(client.player, json_data)
26 | return data_to_hls_url(data, res)
27 |
28 | # experimental: client side video fetching.
29 | def data_to_medium_url(data):
30 | return data["streamingData"]['formats'][0]['url']
31 |
32 | # 360p
33 | # for the kids who begged for this, here you go...
34 | def medium_quality_video_url(video_id):
35 |
36 | json = {
37 | # This can play copyrighted videos.
38 | # See https://github.com/tombulled/innertube/issues/76.
39 | "params": '8AEB',
40 | "videoId": video_id,
41 | "context": Client(constants.client.ANDROID)
42 | }
43 |
44 | # fetch the API.
45 | data = client.post(constants.player, json)
46 |
47 | # i'm lazy. again.
48 | return video.data_to_medium_url(data)
49 |
50 | # You know.
51 | def trending_feeds(region="US", type=""):
52 |
53 | resp_json = []
54 |
55 | json = {
56 | "browseId": "FEtrending",
57 | "context": Client(constants.client.WEB, region)
58 | }
59 |
60 | # SEND TO YOUTUBE IMMEDIATELY
61 | data = client.post(constants.browse, json)
62 | init = data['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']
63 |
64 | # contents at item #3 (3 starting from 0), is intentional
65 | # 0 = 'is sponsored?'
66 | # 1 = '2 random videos'
67 | # 2 = 'shorts'
68 | video_items = init['contents'][3]['itemSectionRenderer']['contents'][0]['shelfRenderer']['content']['expandedShelfContentsRenderer']['items']
69 |
70 | # TODO: add a invidious fallback for certain cases of failure
71 | for x in video_items:
72 | video = x['videoRenderer']
73 | # return in dict
74 | resp_json.append(dict(
75 |
76 | video_id=video['videoId'],
77 | channel_id=video['ownerText']['runs'][0]['navigationEndpoint']['browseEndpoint']['browseId'],
78 | channel_name=video['ownerText']['runs'][0]['text'],
79 | title=video['title']['runs'][0]['text'],
80 | # hottest fix on earth.
81 | # TODO: put KeyError to jail.
82 | description=(video['descriptionSnippet']['runs'][0]['text'] if 'descriptionSnippet' in video else ''),
83 | views=handler.views(video['viewCountText']['simpleText'] if 'viewCountText' in video else 0),
84 | length=handler.to_seconds(video['lengthText']['simpleText'])
85 |
86 | ))
87 |
88 | return resp_json
89 |
90 | def search(query, type="videos"):
91 | json = {
92 | "query": str(query),
93 | "params": constant.param.search(type),
94 | "context": Client(constants.client.WEB)
95 | }
96 |
97 | data = requests.post(constants.search, json)
98 | init = data['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
99 |
100 | for x in video_items:
101 | video = x['videoRenderer']
102 |
103 | # this has been in my head for 2 months, and a quick google search solves it.
104 | resp_json.append(dict(
105 |
106 | video_id=video['videoId'],
107 | channel_id=video['ownerText']['runs'][0]['navigationEndpoint']['browseEndpoint']['browseId'],
108 | channel_name=video['ownerText']['runs'][0]['text'],
109 | title=video['title']['runs'][0]['text'],
110 | description='No descriptions provided.',
111 | views=video['viewCountText']['simpleText'],
112 | published=video['publishedTimeText']['simpleText']
113 |
114 | ))
115 |
116 | def simple_channel_info(id):
117 |
118 | json = {
119 | "context": Client(constants.client.WEB),
120 | "params": constants.param.channel_info,
121 | "browseId": id
122 | }
123 |
124 | # fetch the API.
125 | data = client.post(constants.browse, json)
126 |
127 | return {
128 | "name": data['header']['pageHeaderRenderer']['pageTitle'],
129 | "channel_id": data['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['endpoint']['browseEndpoint']['browseId'],
130 | "profile_picture": data['header']['pageHeaderRenderer']['content']['pageHeaderViewModel']['image']['decoratedAvatarViewModel']['avatar']['avatarViewModel']['image']['sources'][0]['url'],
131 | "subscribers": handler.subscribers(data['header']['pageHeaderRenderer']['content']['pageHeaderViewModel']['metadata']['contentMetadataViewModel']['metadataRows'][1]['metadataParts'][0]['text']['content'])
132 | }
--------------------------------------------------------------------------------
/tuberepair/static/categories.cat:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tuberepair/templates/channel_info.jinja2:
--------------------------------------------------------------------------------
1 |
2 | tag:youtube.com,2008:channel:{{ author_id }}
3 | 2014-09-16T18:07:05.000Z
4 |
5 | {{ author }}
6 | {{ description }}
7 |
8 |
9 |
10 |
11 | {{ author }}
12 | /feeds/api/users/webauditors
13 | {{ author_id }}
14 |
15 | {{ author_id }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tuberepair/templates/channel_playlists.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
8 | http://gdata.youtube.com/feeds/api/users/youtube/playlists
9 | 2011-12-27T20:32:10.466Z
10 |
11 | Playlists of youtube
12 | http://www.youtube.com/img/pic_youtubelogo_123x63.gif
13 |
14 |
15 |
16 |
17 |
18 | {% if continuation %}
19 |
20 | {% endif %}
21 | {% if next_page %}
22 |
23 | {% endif %}
24 |
25 | youtube
26 | http://gdata.youtube.com/feeds/api/users/youtube
27 |
28 | YouTube data API
29 | 90
30 | 1
31 | 25
32 | {% if data %}
33 | {% for playlist in data %}
34 |
35 | http://gdata.youtube.com/feeds/api/users/youtube/playlists/{{playlist['playlistId']}}
36 | 2011-12-19T22:02:40.000Z
37 | 2011-12-27T18:33:18.000Z
38 |
39 | {{playlist['title']}}
40 |
41 |
42 |
43 |
44 |
45 | {{playlist['author']}}
46 | http://gdata.youtube.com/feeds/api/users/youtube
47 |
48 | {{playlist['descriptionHtml']}}
49 | {{playlist['descriptionHtml']}}
50 | {{playlist['descriptionHtml']}}
51 |
52 | {{playlist['videoCount']}}
53 |
54 |
55 |
56 |
57 |
58 | {{playlist['playlistId']}}
59 |
60 | {% endfor %}
61 | {% endif %}
62 |
63 |
--------------------------------------------------------------------------------
/tuberepair/templates/classic/featured.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 | http://gdata.youtube.com/feeds/api/standardfeeds/US/recently_featured
4 |
5 | Spotlight Videos
6 | http://www.gstatic.com/youtube/img/logo.png
7 |
8 |
9 |
10 |
11 | TubeFixer
12 | https://tubefixer.ovh/
13 |
14 | TubeFixer API v2
15 | 1000000
16 | 10
17 | 1
18 |
19 | {% for video in data %}
20 | {% set channel = video['channel_name'] %}
21 | {% set channel_id = video['channel_id'] %}
22 | {% set video_id = video['video_id'] %}
23 | {% set title = video['title'] %}
24 | {% set description = video['description'] %}
25 | {% set time = unix('0') %}
26 | {% set views = video['views'] %}
27 | {% set length = video['length']%}
28 |
29 | tag:youtube.com,2008:video:{{video_id}}
30 | {{time}}
31 | {{time}}
32 |
33 |
34 | {{title}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {{channel}}
45 | {{url}}/feeds/api/users/{{channel_id}}
46 | {{channel_id}}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | Music
65 |
66 |
67 |
68 |
69 | {{channel}}
70 | {{description}}
71 |
72 | youtube
73 |
74 |
75 |
76 |
77 |
78 |
79 | {{title}}
80 | widescreen
81 |
82 | {{time}}
83 | {{channel_id}}
84 | {{video_id}}
85 |
86 |
87 | {% endfor %}
88 |
89 |
--------------------------------------------------------------------------------
/tuberepair/templates/classic/search.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 | http://gdata.youtube.com/feeds/api/videos/
4 |
5 | Videos matching:http://www.gstatic.com/youtube/img/logo.png
6 |
7 |
8 |
11 |
12 | TubeFixer
13 | https://tubefixer.ovh/
14 |
15 | TubeFixer API v2
16 | 1000000
17 | 10
18 | 1
19 | {% if data %}
20 | {% for info in data %}
21 |
22 | tag:youtube.com,2008:video:{{info['videoId']}}
23 | {{'published' in info and unix(info['published']) or unix('0')}}
24 | {{'published' in info and unix(info['published']) or unix('0')}}
25 |
26 |
27 | {{info['title']}}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{info['author']}}
38 | {{url}}/feeds/api/users/{{info['authorId']}}
39 | {{info['authorId']}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Entertainment
58 |
59 |
60 |
61 |
62 | {{info['author']}}
63 |
64 |
65 | youtube
66 |
67 |
68 |
69 |
70 |
71 |
72 | {{info['title']}}
73 | widescreen
74 |
75 | {{'published' in info and unix(info['published']) or unix('0')}}
76 | {{info['authorId']}}
77 | {{info['videoId']}}
78 |
79 |
80 | {% endfor %}
81 | {% endif %}
82 |
--------------------------------------------------------------------------------
/tuberepair/templates/comments.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
8 | tag:youtube.com,2008:channels
9 | 2015-02-16T19:14:12.656Z
10 |
11 | Channels matching: webauditors
12 | http://www.gstatic.com/youtube/img/logo.png
13 |
14 |
15 |
16 |
17 |
18 | {% if continuation %}
19 |
20 | {% endif %}
21 |
22 | YouTube
23 | http://www.youtube.com/
24 |
25 | YouTube data API
26 | 1
27 | 1
28 | 1
29 | {% if data %}
30 | {% for comment in data %}
31 | tag:youtube.com,2008:video:{{comment['videoId']}}:comment:{{unix(comment['published'])}}
32 | {{unix(comment['published'])}}
33 |
34 | Comment from {{comment['author']}}
35 | {{comment['content']}}
36 |
37 |
38 |
39 |
40 | {{comment['author']}}
41 | {{comment['authorId']}}
42 | {{comment['authorId']}}
43 |
44 | {{comment['authorId']}}
45 | 1
46 | {{comment['videoId']}}
47 |
48 | {% endfor %}
49 | {% endif %}
--------------------------------------------------------------------------------
/tuberepair/templates/featured.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 | tag:youtube.com,2008:playlist:8E2186857EE27746
10 | 2012-08-23T12:33:58.000Z
11 |
12 |
13 |
14 | http://www.gstatic.com/youtube/img/logo.png
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{url}}
23 |
24 |
25 | YouTube data API
26 | {{data|count}}
27 | {{data|count}}
28 | 1
29 |
30 | {% for video in data %}
31 | {% set channel = video['channel_name'] %}
32 | {% set channel_id = video['channel_id'] %}
33 | {% set video_id = video['video_id'] %}
34 | {% set title = video['title'] %}
35 | {% set description = video['description'] %}
36 | {% set time = unix('0') %}
37 | {% set views = video['views'] %}
38 | {% set length = video['length'] %}
39 |
40 | tag:youtube.com,2008:playlist:{{video_id}}:{{video_id}}
41 | {{ time }}
42 | {{ time }}
43 |
44 |
45 | {{title}}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {{channel}}
56 | /
57 | {{channel_id}}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | Howto
76 |
77 |
78 |
79 | {{channel_id}}
80 | {{description}}
81 |
82 | youtube
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {{description}}
95 |
96 | {{time}}
97 | {{channel_id}}
98 | {{video_id}}
99 |
100 |
101 | 1970-08-22
102 |
103 |
104 | 1
105 |
106 | {% endfor %}
107 |
108 |
--------------------------------------------------------------------------------
/tuberepair/templates/playlist_videos.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
8 | tag:youtube.com,2008:playlist:8E2186857EE27746
9 | 2012-08-23T12:33:58.000Z
10 |
11 |
12 |
13 | https://www.gstatic.com/youtube/img/logo.png
14 |
15 |
16 |
17 |
18 |
19 |
20 | YouTube
21 | {{url}}/feeds/api/users/
22 |
23 |
24 | YouTube video API
25 | 9
26 | 9
27 | 1
28 | {% if data %}
29 | {% for video in data %}
30 |
31 | tag:youtube.com,2008:playlist:{{video['videoId']}}:{{video['videoId']}}
32 | 2012-08-23T12:33:58.000Z
33 | 2012-08-23T12:33:58.000Z
34 |
35 |
36 | {{video['title']}}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{video['author']}}
46 | {{url}}/feeds/api/users/{{video['authorId']}}
47 | {{video['authorId']}}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Paris ,FR
61 |
62 | Howto
63 |
64 |
65 |
66 | {{video['authorId']}}
67 |
68 |
69 | youtube
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | {{video['title']}}
82 |
83 | 1969-12-31T12:33:58.000Z
84 | {{video['authorId']}}
85 | {{video['videoId']}}
86 |
87 |
88 | 1970-01-01
89 |
90 |
91 | {{video['index']}}
92 |
93 | {% endfor %}
94 | {% endif %}
95 |
96 |
--------------------------------------------------------------------------------
/tuberepair/templates/search_results.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
8 | tag:youtube.com,2008:channels
9 | 2015-02-16T19:14:12.656Z
10 |
11 | Channels matching: webauditors
12 | http://www.gstatic.com/youtube/img/logo.png
13 |
14 |
15 |
16 |
17 |
18 | {% if next_page %}
19 |
20 | {% endif %}
21 |
22 | YouTube
23 | http://www.youtube.com/
24 |
25 | YouTube data API
26 | 1
27 | 1
28 | 1
29 | {% if data %}
30 | {% for info in data %}
31 |
32 | tag:youtube.com,2008:playlist:{{info['videoId']}}:{{info['videoId']}}
33 | {{'published' in info and unix(info['published']) or unix('0')}}
34 | {{'published' in info and unix(info['published']) or unix('0')}}
35 |
36 |
37 | {{info['title']}}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {{info['author']}}
47 | https://gdata.youtube.com/feeds/api/users/{{info['authorId']}}
48 | {{info['authorId']}}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Paris ,FR
62 |
63 | Howto
64 |
65 |
66 |
67 | {{info['authorId']}}
68 | {{info['description']}}
69 |
70 | youtube
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {{info['title']}}
83 |
84 | {{'published' in info and unix(info['published']) or unix('0')}}
85 | {{info['authorId']}}
86 | {{info['videoId']}}
87 |
88 |
89 | 1970-08-22
90 |
91 |
92 | 1
93 |
94 | {% endfor %}
95 | {% endif %}
--------------------------------------------------------------------------------
/tuberepair/templates/search_results_channel.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
8 | tag:youtube.com,2008:channels
9 | 2015-02-16T19:14:12.656Z
10 |
11 | Channels matching: webauditors
12 | http://www.gstatic.com/youtube/img/logo.png
13 |
14 |
15 |
16 |
17 |
18 | {% if next_page %}
19 |
20 | {% endif %}
21 |
22 | YouTube
23 | http://www.youtube.com/
24 |
25 | YouTube data API
26 | 1
27 | 1
28 | 1
29 | {% if data %}
30 | {% for channels in data %}
31 |
32 | tag:youtube.com,2008:channel:{{channels['authorId']}}
33 | 2014-09-16T18:07:05.000Z
34 |
35 | {{channels['author']}}
36 |
37 |
38 |
39 |
40 |
41 | {{channels['author']}}
42 | {{url}}/feeds/api/users/webauditors
43 | {{channels['authorId']}}
44 |
45 | {{channels['authorId']}}
46 |
47 |
48 |
49 |
50 | {% endfor %}
51 | {% endif %}
52 |
53 |
--------------------------------------------------------------------------------
/tuberepair/templates/uploads.jinja2:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ url }}/feeds/api/standardfeeds/US/recently_featured
4 |
5 | Spotlight Videoshttp://www.gstatic.com/youtube/img/logo.png
6 |
7 |
8 |
9 | {% if continuation %}
10 |
11 | {% endif %}
12 |
13 | YouTube
14 |
15 |
16 | 20
17 | 25
18 | 1
19 | {% if data %}
20 | {% for uploads in data %}
21 |
22 | tag:youtube.com,2008:video:{{ uploads['videoId'] }}
23 | {{ unix(uploads['published']) }}
24 | {{ unix(uploads['published']) }}
25 |
26 |
27 | {{ uploads['title'] }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | {{ uploads['author'] }}
39 | {{ url }}/feeds/api/users/{{ uploads['author'] }}
40 | {{ uploads['authorId'] }}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Music
59 |
60 |
61 |
62 |
63 | {{ uploads['author'] }}
64 | {{ uploads['description'] }}
65 | keywords
66 | youtube
67 |
68 |
69 |
70 |
71 |
72 |
73 | {{ uploads['title'] }}
74 | widescreen
75 |
76 | {{ unix(uploads['published']) }}
77 | {{ uploads['authorId'] }}
78 | {{ uploads['videoId'] }}
79 |
80 |
81 | {% endfor %}
82 | {% endif %}
83 |
--------------------------------------------------------------------------------
/tuberepair/templates/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{version}}
7 |
43 |
44 |
45 |
46 |
TubeRepair server
47 |
{{version}}
48 |
49 |
This was written in Python as a backend for the TubeRepair tweak by bag.xml (et al).