This commit is contained in:
CypherpunkSamurai 2023-02-16 13:25:03 +05:30
commit 9aab6148ba
756 changed files with 155754 additions and 0 deletions

3
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,3 @@
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
- [ ] I have tested the API against [NewPipe](https://github.com/TeamNewPipe/NewPipe).
- [ ] I agree to create a pull request for [NewPipe](https://github.com/TeamNewPipe/NewPipe) as soon as possible to make it compatible with the changed API.

12
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,12 @@
version: 2
updates:
# Maintain dependencies for Gradle
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "daily"
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

51
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,51 @@
name: CI
on:
schedule:
# once per day
- cron: 0 0 * * *
push:
branches:
- dev
- master
pull_request:
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
# See gradle file for difference between downloaders
- name: Build and run Tests
run: |
if [[ $GITHUB_EVENT_NAME == 'schedule' ]]; then
echo running with real downloader
./gradlew check --stacktrace -Ddownloader=REAL
else
echo running with mock downloader
./gradlew check --stacktrace -Ddownloader=MOCK
fi
- name: Upload test reports when failure occurs
uses: actions/upload-artifact@v3
if: failure()
with:
name: NewPipeExtractor-test-reports
path: extractor/build/reports/tests/test/**

38
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Build and deploy JavaDocs
on:
push:
branches:
- master
permissions:
# The generated docs are written to the `gh-pages` branch.
contents: write
jobs:
build-and-deploy-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 8
uses: actions/setup-java@v3
with:
java-version: '8'
distribution: 'temurin'
- name: Cache Gradle dependencies
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build JavaDocs
run: ./gradlew aggregatedJavadocs
- name: Deploy JavaDocs
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build/docs

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
.gradle
build/
.idea
local.properties
out/
*.iml
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# vscode / eclipse files
*.classpath
*.project
*.settings
**/bin
**.vscode
*.code-workspace

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

57
README.md Normal file
View File

@ -0,0 +1,57 @@
# NewPipe Extractor
[![CI](https://github.com/TeamNewPipe/NewPipeExtractor/actions/workflows/ci.yml/badge.svg?branch=dev&event=schedule)](https://github.com/TeamNewPipe/NewPipeExtractor/actions/workflows/ci.yml) [![JIT Pack Badge](https://jitpack.io/v/TeamNewPipe/NewPipeExtractor.svg)](https://jitpack.io/#TeamNewPipe/NewPipeExtractor) [JDoc](https://teamnewpipe.github.io/NewPipeExtractor/javadoc/) • [Documentation](https://teamnewpipe.github.io/documentation/)
NewPipe Extractor is a library for extracting things from streaming sites. It is a core component of [NewPipe](https://github.com/TeamNewPipe/NewPipe), but could be used independently.
## Usage
NewPipe Extractor is available at JitPack's Maven repo.
If you're using Gradle, you could add NewPipe Extractor as a dependency with the following steps:
1. Add `maven { url 'https://jitpack.io' }` to the `repositories` in your `build.gradle`.
2. Add `implementation 'com.github.TeamNewPipe:NewPipeExtractor:INSERT_VERSION_HERE'` to the `dependencies` in your `build.gradle`. Replace `INSERT_VERSION_HERE` with the [latest release](https://github.com/TeamNewPipe/NewPipeExtractor/releases/latest).
**Note:** To use NewPipe Extractor in projects with a `minSdk` below 26, [API desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) is required. If the `minSdk` is below 19, the `desugar_jdk_libs_nio` artifact is required, which requires Android Gradle Plugin (AGP) version 7.4.0.
### Testing changes
To test changes quickly you can build the library locally. A good approach would be to add something like the following to your `settings.gradle`:
```groovy
includeBuild('../NewPipeExtractor') {
dependencySubstitution {
substitute module('com.github.TeamNewPipe:NewPipeExtractor') with project(':extractor')
}
}
```
Another approach would be to use the local Maven repository, here's a gist of how to use it:
1. Add `mavenLocal()` in your project `repositories` list (usually as the first entry to give priority above the others).
2. It's _recommended_ that you change the `version` of this library (e.g. `LOCAL_SNAPSHOT`).
3. Run gradle's `ìnstall` task to deploy this library to your local repository (using the wrapper, present in the root of this project: `./gradlew install`)
4. Change the dependency version used in your project to match the one you chose in step 2 (`implementation 'com.github.TeamNewPipe:NewPipeExtractor:LOCAL_SNAPSHOT'`)
> Tip for Android Studio users: After you make changes and run the `install` task, use the menu option `File → "Sync with File System"` to refresh the library in your project.
## Supported sites
The following sites are currently supported:
- YouTube
- SoundCloud
- media.ccc.de
- PeerTube (no P2P)
- Bandcamp
## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
NewPipe is Free Software: You can use, study share and improve it at your
will. Specifically you can redistribute and/or modify it under the terms of the
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) as
published by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

83
build.gradle Normal file
View File

@ -0,0 +1,83 @@
allprojects {
apply plugin: 'java-library'
apply plugin: 'maven-publish'
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
version 'v0.22.1'
group 'com.github.TeamNewPipe'
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
}
afterEvaluate {
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
}
}
ext {
nanojsonVersion = "1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
spotbugsVersion = "4.7.3"
junitVersion = "5.9.2"
checkstyleVersion = "10.4"
}
}
dependencies {
api project(':extractor')
implementation project(':timeago-parser')
}
subprojects {
task sourcesJar(type: Jar, dependsOn: classes) {
archiveClassifier.set('sources')
from sourceSets.main.allSource
}
tasks.withType(Test) {
testLogging {
events "skipped", "failed"
showStandardStreams = true
exceptionFormat = 'full'
}
}
artifacts {
archives sourcesJar
}
}
// https://discuss.gradle.org/t/best-approach-gradle-multi-module-project-generate-just-one-global-javadoc/18657/21
task aggregatedJavadocs(type: Javadoc, group: 'Documentation') {
destinationDir = file("$buildDir/docs/javadoc")
title = "$project.name $version"
// options.memberLevel = JavadocMemberLevel.PRIVATE
options.links 'https://docs.oracle.com/javase/8/docs/api/'
options.encoding 'UTF-8'
// Fixes unknown tag @implNote; the other two were added precautionary
options.tags = [
"apiNote:a:API Note:",
"implSpec:a:Implementation Requirements:",
"implNote:a:Implementation Note:"
]
subprojects.each { project ->
project.tasks.withType(Javadoc).each { javadocTask ->
source += javadocTask.source
classpath += javadocTask.classpath
excludes += javadocTask.excludes
includes += javadocTask.includes
}
}
}

193
checkstyle/checkstyle.xml Normal file
View File

@ -0,0 +1,193 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<!--
If you set the basedir property below, then all reported file
names will be relative to the specified directory. See
https://checkstyle.org/5.x/config.html#Checker
<property name="basedir" value="${basedir}"/>
-->
<property name="severity" value="error"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- Checks that a package-info.java file exists for each package. -->
<!-- See https://checkstyle.org/config_javadoc.html#JavadocPackage -->
<!--<module name="JavadocPackage"/>-->
<!-- Checks whether files end with a new line. -->
<!-- See https://checkstyle.org/config_misc.html#NewlineAtEndOfFile -->
<module name="NewlineAtEndOfFile"/>
<!-- Checks that property files contain the same keys. -->
<!-- See https://checkstyle.org/config_misc.html#Translation -->
<module name="Translation"/>
<!-- Checks for Size Violations. -->
<!-- See https://checkstyle.org/config_sizes.html -->
<module name="FileLength"/>
<module name="LineLength">
<property name="max" value="100"/>
<property name="fileExtensions" value="java"/>
</module>
<!-- Checks for whitespace -->
<!-- See https://checkstyle.org/config_whitespace.html -->
<module name="FileTabCharacter"/>
<!-- Miscellaneous other checks. -->
<!-- See https://checkstyle.org/config_misc.html -->
<module name="RegexpSingleline">
<property name="format" value="\s+$"/>
<property name="minimum" value="0"/>
<property name="maximum" value="0"/>
<property name="message" value="Line has trailing spaces."/>
</module>
<!-- Checks for Headers -->
<!-- See https://checkstyle.org/config_header.html -->
<!-- <module name="Header"> -->
<!-- <property name="headerFile" value="${checkstyle.header.file}"/> -->
<!-- <property name="fileExtensions" value="java"/> -->
<!-- </module> -->
<module name="SuppressWarningsFilter" />
<module name="SuppressWithPlainTextCommentFilter"/>
<module name="TreeWalker">
<!-- Checks for Javadoc comments. -->
<!-- See https://checkstyle.org/config_javadoc.html -->
<module name="InvalidJavadocPosition"/>
<module name="JavadocMethod">
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
</module>
<module name="JavadocType"/>
<!--<module name="JavadocVariable"/>-->
<module name="JavadocStyle">
<property name="checkFirstSentence" value="false"/>
</module>
<!--<module name="MissingJavadocMethod"/>-->
<!-- Checks for Naming Conventions. -->
<!-- See https://checkstyle.org/config_naming.html -->
<module name="ConstantName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName">
<property name="format" value="^(TAG|DEBUG|[a-z][a-zA-Z0-9]*)$"/>
</module>
<module name="MethodName"/>
<module name="PackageName"/>
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<!-- Checks for imports -->
<!-- See https://checkstyle.org/config_import.html -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"> <!-- defaults to sun.* packages -->
<property name="illegalClasses" value="
org.jetbrains.annotations.Nullable,
org.jetbrains.annotations.NotNull,
androidx.annotation.Nullable,
androidx.annotation.NonNull,
io.reactivex.rxjava3.annotations.NonNull,
io.reactivex.rxjava3.annotations.Nullable" />
</module>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!-- Checks for Size Violations. -->
<!-- See https://checkstyle.org/config_sizes.html -->
<module name="MethodLength">
<property name="severity" value="warning"/>
</module>
<module name="ParameterNumber">
<property name="severity" value="warning"/>
</module>
<!-- Checks for whitespace -->
<!-- See https://checkstyle.org/config_whitespace.html -->
<module name="EmptyForIteratorPad"/>
<module name="GenericWhitespace"/>
<module name="MethodParamPad"/>
<module name="NoWhitespaceAfter"/>
<module name="NoWhitespaceBefore"/>
<module name="OperatorWrap"/>
<module name="ParenPad"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<!-- Modifier Checks -->
<!-- See https://checkstyle.org/config_modifiers.html -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Checks for blocks. You know, those {}'s -->
<!-- See https://checkstyle.org/config_blocks.html -->
<module name="AvoidNestedBlocks"/>
<module name="EmptyBlock"/>
<module name="LeftCurly"/>
<module name="NeedBraces"/>
<module name="RightCurly"/>
<!-- Checks for common coding problems -->
<!-- See https://checkstyle.org/config_coding.html -->
<module name="EmptyStatement"/>
<module name="EqualsHashCode">
<property name="severity" value="warning"/>
</module>
<module name="HiddenField">
<property name="ignoreConstructorParameter" value="true"/>
<property name="ignoreSetter" value="true"/>
</module>
<module name="IllegalInstantiation"/>
<module name="InnerAssignment"/>
<!--<module name="MagicNumber"/>-->
<!--<module name="MissingSwitchDefault">
<property name="severity" value="warning"/>
</module>-->
<module name="MultipleVariableDeclarations"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="FinalLocalVariable">
<property name="tokens" value="VARIABLE_DEF,PARAMETER_DEF"/>
<property name="validateEnhancedForLoopVariable" value="true"/>
</module>
<!-- Checks for class design -->
<!-- See https://checkstyle.org/config_design.html -->
<!--<module name="DesignForExtension"/>-->
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InterfaceIsType"/>
<!--<module name="VisibilityModifier">
<property name="ignoreAnnotationCanonicalNames" value="State,ColumnInfo"/>
<property name="severity" value="warning"/>
</module>-->
<!-- Miscellaneous other checks. -->
<!-- See https://checkstyle.org/config_misc.html -->
<module name="ArrayTypeStyle"/>
<module name="FinalParameters"/>
<!--<module name="TodoComment">
<property name="format" value="(TODO:|FIXME:)"/>
<property name="severity" value="warning"/>
</module>-->
<module name="UpperEll"/>
<module name="SuppressWarningsHolder" />
</module>
</module>

45
extractor/build.gradle Normal file
View File

@ -0,0 +1,45 @@
plugins {
id 'checkstyle'
}
test {
// Pass on downloader type to tests for different CI jobs. See DownloaderFactory.java and ci.yml
if (System.properties.containsKey('downloader')) {
systemProperty('downloader', System.getProperty('downloader'))
}
useJUnitPlatform()
dependsOn checkstyleMain // run checkstyle when testing
}
checkstyle {
getConfigDirectory().set(rootProject.file("checkstyle"))
ignoreFailures false
showViolations true
toolVersion checkstyleVersion
}
checkstyleTest {
enabled false // do not checkstyle test files
}
dependencies {
implementation project(':timeago-parser')
implementation "com.github.TeamNewPipe:nanojson:$nanojsonVersion"
implementation 'org.jsoup:jsoup:1.15.3'
implementation "com.github.spotbugs:spotbugs-annotations:$spotbugsVersion"
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation 'org.mozilla:rhino:1.7.13'
checkstyle "com.puppycrawl.tools:checkstyle:$checkstyleVersion"
testImplementation platform("org.junit:junit-bom:$junitVersion")
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
testImplementation 'com.google.code.gson:gson:2.10.1'
}

View File

@ -0,0 +1,46 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.List;
/**
* Collectors are used to simplify the collection of information
* from extractors
* @param <I> the item type
* @param <E> the extractor type
*/
public interface Collector<I, E> {
/**
* Try to add an extractor to the collection
* @param extractor the extractor to add
*/
void commit(E extractor);
/**
* Try to extract the item from an extractor without adding it to the collection
* @param extractor the extractor to use
* @return the item
* @throws ParsingException thrown if there is an error extracting the
* <b>required</b> fields of the item.
*/
I extract(E extractor) throws ParsingException;
/**
* Get all items
* @return the items
*/
List<I> getItems();
/**
* Get all errors
* @return the errors
*/
List<Throwable> getErrors();
/**
* Reset all collected items and errors
*/
void reset();
}

View File

@ -0,0 +1,154 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Objects;
public abstract class Extractor {
/**
* {@link StreamingService} currently related to this extractor.<br>
* Useful for getting other things from a service (like the url handlers for
* cleaning/accepting/get id from urls).
*/
private final StreamingService service;
private final LinkHandler linkHandler;
@Nullable
private Localization forcedLocalization = null;
@Nullable
private ContentCountry forcedContentCountry = null;
private boolean pageFetched = false;
// called like this to prevent checkstyle errors about "hiding a field"
private final Downloader downloader;
protected Extractor(final StreamingService service, final LinkHandler linkHandler) {
this.service = Objects.requireNonNull(service, "service is null");
this.linkHandler = Objects.requireNonNull(linkHandler, "LinkHandler is null");
this.downloader = Objects.requireNonNull(NewPipe.getDownloader(), "downloader is null");
}
/**
* @return The {@link LinkHandler} of the current extractor object (e.g. a ChannelExtractor
* should return a channel url handler).
*/
@Nonnull
public LinkHandler getLinkHandler() {
return linkHandler;
}
/**
* Fetch the current page.
*
* @throws IOException if the page can not be loaded
* @throws ExtractionException if the pages content is not understood
*/
public void fetchPage() throws IOException, ExtractionException {
if (pageFetched) {
return;
}
onFetchPage(downloader);
pageFetched = true;
}
protected void assertPageFetched() {
if (!pageFetched) {
throw new IllegalStateException("Page is not fetched. Make sure you call fetchPage()");
}
}
protected boolean isPageFetched() {
return pageFetched;
}
/**
* Fetch the current page.
*
* @param downloader the downloader to use
* @throws IOException if the page can not be loaded
* @throws ExtractionException if the pages content is not understood
*/
@SuppressWarnings("HiddenField")
public abstract void onFetchPage(@Nonnull Downloader downloader)
throws IOException, ExtractionException;
@Nonnull
public String getId() throws ParsingException {
return linkHandler.getId();
}
/**
* Get the name
*
* @return the name
* @throws ParsingException if the name cannot be extracted
*/
@Nonnull
public abstract String getName() throws ParsingException;
@Nonnull
public String getOriginalUrl() throws ParsingException {
return linkHandler.getOriginalUrl();
}
@Nonnull
public String getUrl() throws ParsingException {
return linkHandler.getUrl();
}
@Nonnull
public String getBaseUrl() throws ParsingException {
return linkHandler.getBaseUrl();
}
@Nonnull
public StreamingService getService() {
return service;
}
public int getServiceId() {
return service.getServiceId();
}
public Downloader getDownloader() {
return downloader;
}
/*//////////////////////////////////////////////////////////////////////////
// Localization
//////////////////////////////////////////////////////////////////////////*/
public void forceLocalization(final Localization localization) {
this.forcedLocalization = localization;
}
public void forceContentCountry(final ContentCountry contentCountry) {
this.forcedContentCountry = contentCountry;
}
@Nonnull
public Localization getExtractorLocalization() {
return forcedLocalization == null ? getService().getLocalization() : forcedLocalization;
}
@Nonnull
public ContentCountry getExtractorContentCountry() {
return forcedContentCountry == null ? getService().getContentCountry()
: forcedContentCountry;
}
@Nonnull
public TimeAgoParser getTimeAgoParser() {
return getService().getTimeAgoParser(getExtractorLocalization());
}
}

View File

@ -0,0 +1,111 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public abstract class Info implements Serializable {
private final int serviceId;
/**
* Id of this Info object <br>
* e.g. Youtube: https://www.youtube.com/watch?v=RER5qCTzZ7 &gt; RER5qCTzZ7
*/
private final String id;
/**
* Different than the {@link #originalUrl} in the sense that it <i>may</i> be set as a cleaned
* url.
*
* @see LinkHandler#getUrl()
* @see Extractor#getOriginalUrl()
*/
private final String url;
/**
* The url used to start the extraction of this {@link Info} object.
*
* @see Extractor#getOriginalUrl()
*/
private String originalUrl;
private final String name;
private final List<Throwable> errors = new ArrayList<>();
public void addError(final Throwable throwable) {
this.errors.add(throwable);
}
public void addAllErrors(final Collection<Throwable> throwables) {
this.errors.addAll(throwables);
}
public Info(final int serviceId,
final String id,
final String url,
final String originalUrl,
final String name) {
this.serviceId = serviceId;
this.id = id;
this.url = url;
this.originalUrl = originalUrl;
this.name = name;
}
public Info(final int serviceId, final LinkHandler linkHandler, final String name) {
this(serviceId,
linkHandler.getId(),
linkHandler.getUrl(),
linkHandler.getOriginalUrl(),
name);
}
@Override
public String toString() {
final String ifDifferentString
= url.equals(originalUrl) ? "" : " (originalUrl=\"" + originalUrl + "\")";
return getClass().getSimpleName() + "[url=\"" + url + "\"" + ifDifferentString
+ ", name=\"" + name + "\"]";
}
// if you use an api and want to handle the website url
// overriding original url is essential
public void setOriginalUrl(final String originalUrl) {
this.originalUrl = originalUrl;
}
public int getServiceId() {
return serviceId;
}
public StreamingService getService() {
try {
return NewPipe.getService(serviceId);
} catch (final ExtractionException e) {
// this should be unreachable, as serviceId certainly refers to a valid service
throw new RuntimeException("Info object has invalid service id", e);
}
}
public String getId() {
return id;
}
public String getUrl() {
return url;
}
public String getOriginalUrl() {
return originalUrl;
}
public String getName() {
return name;
}
public List<Throwable> getErrors() {
return errors;
}
}

View File

@ -0,0 +1,77 @@
package org.schabi.newpipe.extractor;
/*
* Created by Christian Schabesberger on 11.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* InfoItem.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import java.io.Serializable;
public abstract class InfoItem implements Serializable {
private final InfoType infoType;
private final int serviceId;
private final String url;
private final String name;
private String thumbnailUrl;
public InfoItem(final InfoType infoType,
final int serviceId,
final String url,
final String name) {
this.infoType = infoType;
this.serviceId = serviceId;
this.url = url;
this.name = name;
}
public InfoType getInfoType() {
return infoType;
}
public int getServiceId() {
return serviceId;
}
public String getUrl() {
return url;
}
public String getName() {
return name;
}
public void setThumbnailUrl(final String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[url=\"" + url + "\", name=\"" + name + "\"]";
}
public enum InfoType {
STREAM,
PLAYLIST,
CHANNEL,
COMMENT
}
}

View File

@ -0,0 +1,9 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
public interface InfoItemExtractor {
String getName() throws ParsingException;
String getUrl() throws ParsingException;
String getThumbnailUrl() throws ParsingException;
}

View File

@ -0,0 +1,111 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* InfoItemsCollector.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemExtractor>
implements Collector<I, E> {
private final List<I> itemList = new ArrayList<>();
private final List<Throwable> errors = new ArrayList<>();
private final int serviceId;
@Nullable
private final Comparator<I> comparator;
/**
* Create a new collector with no comparator / sorting function
* @param serviceId the service id
*/
public InfoItemsCollector(final int serviceId) {
this(serviceId, null);
}
/**
* Create a new collector
* @param serviceId the service id
*/
public InfoItemsCollector(final int serviceId, @Nullable final Comparator<I> comparator) {
this.serviceId = serviceId;
this.comparator = comparator;
}
@Override
public List<I> getItems() {
if (comparator != null) {
itemList.sort(comparator);
}
return Collections.unmodifiableList(itemList);
}
@Override
public List<Throwable> getErrors() {
return Collections.unmodifiableList(errors);
}
@Override
public void reset() {
itemList.clear();
errors.clear();
}
/**
* Add an error
* @param error the error
*/
protected void addError(final Exception error) {
errors.add(error);
}
/**
* Add an item
* @param item the item
*/
protected void addItem(final I item) {
itemList.add(item);
}
/**
* Get the service id
* @return the service id
*/
public int getServiceId() {
return serviceId;
}
@Override
public void commit(final E extractor) {
try {
addItem(extract(extractor));
} catch (final FoundAdException ae) {
// found an ad. Maybe a debug line could be placed here
} catch (final ParsingException e) {
addError(e);
}
}
}

View File

@ -0,0 +1,132 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
/**
* Base class to extractors that have a list (e.g. playlists, users).
* @param <R> the info item type this list extractor provides
*/
public abstract class ListExtractor<R extends InfoItem> extends Extractor {
/**
* Constant that should be returned whenever
* a list has an unknown number of items.
*/
public static final long ITEM_COUNT_UNKNOWN = -1;
/**
* Constant that should be returned whenever a list has an
* infinite number of items. For example a YouTube mix.
*/
public static final long ITEM_COUNT_INFINITE = -2;
/**
* Constant that should be returned whenever a list
* has an unknown number of items bigger than 100.
*/
public static final long ITEM_COUNT_MORE_THAN_100 = -3;
public ListExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
/**
* A {@link InfoItemsPage InfoItemsPage} corresponding to the initial page
* where the items are from the initial request and the nextPage relative to it.
*
* @return a {@link InfoItemsPage} corresponding to the initial page
*/
@Nonnull
public abstract InfoItemsPage<R> getInitialPage() throws IOException, ExtractionException;
/**
* Get a list of items corresponding to the specific requested page.
*
* @param page any page got from the exclusive implementation of the list extractor
* @return a {@link InfoItemsPage} corresponding to the requested page
* @see InfoItemsPage#getNextPage()
*/
public abstract InfoItemsPage<R> getPage(Page page) throws IOException, ExtractionException;
@Nonnull
@Override
public ListLinkHandler getLinkHandler() {
return (ListLinkHandler) super.getLinkHandler();
}
/*//////////////////////////////////////////////////////////////////////////
// Inner
//////////////////////////////////////////////////////////////////////////*/
/**
* A class that is used to wrap a list of gathered items and eventual errors, it
* also contains a field that points to the next available page ({@link #nextPage}).
* @param <T> the info item type that this page is supposed to store and provide
*/
public static class InfoItemsPage<T extends InfoItem> {
private static final InfoItemsPage<InfoItem> EMPTY =
new InfoItemsPage<>(Collections.emptyList(), null, Collections.emptyList());
/**
* A convenient method that returns a representation of an empty page.
*
* @return a type-safe page with the list of items and errors empty and the nextPage set to
* {@code null}.
*/
public static <T extends InfoItem> InfoItemsPage<T> emptyPage() {
//noinspection unchecked
return (InfoItemsPage<T>) EMPTY;
}
/**
* The current list of items of this page
*/
private final List<T> itemsList;
/**
* Url pointing to the next page relative to this one
*
* @see ListExtractor#getPage(Page)
* @see Page
*/
private final Page nextPage;
/**
* Errors that happened during the extraction
*/
private final List<Throwable> errors;
public InfoItemsPage(final InfoItemsCollector<T, ?> collector, final Page nextPage) {
this(collector.getItems(), nextPage, collector.getErrors());
}
public InfoItemsPage(final List<T> itemsList,
final Page nextPage,
final List<Throwable> errors) {
this.itemsList = itemsList;
this.nextPage = nextPage;
this.errors = errors;
}
public boolean hasNextPage() {
return Page.isValid(nextPage);
}
public List<T> getItems() {
return itemsList;
}
public Page getNextPage() {
return nextPage;
}
public List<Throwable> getErrors() {
return errors;
}
}
}

View File

@ -0,0 +1,60 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import java.util.List;
public abstract class ListInfo<T extends InfoItem> extends Info {
private List<T> relatedItems;
private Page nextPage = null;
private final List<String> contentFilters;
private final String sortFilter;
public ListInfo(final int serviceId,
final String id,
final String url,
final String originalUrl,
final String name,
final List<String> contentFilter,
final String sortFilter) {
super(serviceId, id, url, originalUrl, name);
this.contentFilters = contentFilter;
this.sortFilter = sortFilter;
}
public ListInfo(final int serviceId,
final ListLinkHandler listUrlIdHandler,
final String name) {
super(serviceId, listUrlIdHandler, name);
this.contentFilters = listUrlIdHandler.getContentFilters();
this.sortFilter = listUrlIdHandler.getSortFilter();
}
public List<T> getRelatedItems() {
return relatedItems;
}
public void setRelatedItems(final List<T> relatedItems) {
this.relatedItems = relatedItems;
}
public boolean hasNextPage() {
return Page.isValid(nextPage);
}
public Page getNextPage() {
return nextPage;
}
public void setNextPage(final Page page) {
this.nextPage = page;
}
public List<String> getContentFilters() {
return contentFilters;
}
public String getSortFilter() {
return sortFilter;
}
}

View File

@ -0,0 +1,168 @@
package org.schabi.newpipe.extractor;
/*
* Created by Adam Howard on 08/11/15.
*
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
* and Adam Howard <achdisposable1@gmail.com> 2015
*
* MediaFormat.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import java.util.Arrays;
import java.util.function.Function;
/**
* Static data about various media formats support by NewPipe, eg mime type, extension
*/
@SuppressWarnings("MethodParamPad") // we want the media format table below to be aligned
public enum MediaFormat {
// @formatter:off
//video and audio combined formats
// id name suffix mimeType
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
v3GPP (0x10, "3GPP", "3gp", "video/3gpp"),
WEBM (0x20, "WebM", "webm", "video/webm"),
// audio formats
M4A (0x100, "m4a", "m4a", "audio/mp4"),
WEBMA (0x200, "WebM", "webm", "audio/webm"),
MP3 (0x300, "MP3", "mp3", "audio/mpeg"),
OPUS (0x400, "opus", "opus", "audio/opus"),
OGG (0x500, "ogg", "ogg", "audio/ogg"),
WEBMA_OPUS(0x200, "WebM Opus", "webm", "audio/webm"),
// subtitles formats
VTT (0x1000, "WebVTT", "vtt", "text/vtt"),
TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"),
TRANSCRIPT1(0x3000, "TranScript v1", "srv1", "text/xml"),
TRANSCRIPT2(0x4000, "TranScript v2", "srv2", "text/xml"),
TRANSCRIPT3(0x5000, "TranScript v3", "srv3", "text/xml"),
SRT (0x6000, "SubRip file format", "srt", "text/srt");
// @formatter:on
public final int id;
public final String name;
public final String suffix;
public final String mimeType;
MediaFormat(final int id, final String name, final String suffix, final String mimeType) {
this.id = id;
this.name = name;
this.suffix = suffix;
this.mimeType = mimeType;
}
private static <T> T getById(final int id,
final Function<MediaFormat, T> field,
final T orElse) {
return Arrays.stream(MediaFormat.values())
.filter(mediaFormat -> mediaFormat.id == id)
.map(field)
.findFirst()
.orElse(orElse);
}
/**
* Return the friendly name of the media format with the supplied id
*
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
* @return the friendly name of the MediaFormat associated with this ids,
* or an empty String if none match it.
*/
public static String getNameById(final int id) {
return getById(id, MediaFormat::getName, "");
}
/**
* Return the file extension of the media format with the supplied id
*
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
* @return the file extension of the MediaFormat associated with this ids,
* or an empty String if none match it.
*/
public static String getSuffixById(final int id) {
return getById(id, MediaFormat::getSuffix, "");
}
/**
* Return the MIME type of the media format with the supplied id
*
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
* @return the MIME type of the MediaFormat associated with this ids,
* or an empty String if none match it.
*/
public static String getMimeById(final int id) {
return getById(id, MediaFormat::getMimeType, null);
}
/**
* Return the MediaFormat with the supplied mime type
*
* @return MediaFormat associated with this mime type,
* or null if none match it.
*/
public static MediaFormat getFromMimeType(final String mimeType) {
return Arrays.stream(MediaFormat.values())
.filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType))
.findFirst()
.orElse(null);
}
/**
* Get the media format by its id.
*
* @param id the id
* @return the id of the media format or null.
*/
public static MediaFormat getFormatById(final int id) {
return getById(id, mediaFormat -> mediaFormat, null);
}
public static MediaFormat getFromSuffix(final String suffix) {
return Arrays.stream(MediaFormat.values())
.filter(mediaFormat -> mediaFormat.suffix.equals(suffix))
.findFirst()
.orElse(null);
}
/**
* Get the name of the format
*
* @return the name of the format
*/
public String getName() {
return name;
}
/**
* Get the filename extension
*
* @return the filename extension
*/
public String getSuffix() {
return suffix;
}
/**
* Get the mime type
*
* @return the mime type
*/
public String getMimeType() {
return mimeType;
}
}

View File

@ -0,0 +1,78 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.stream.Description;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
public class MetaInfo implements Serializable {
private String title = "";
private Description content;
private List<URL> urls = new ArrayList<>();
private List<String> urlTexts = new ArrayList<>();
public MetaInfo(@Nonnull final String title,
@Nonnull final Description content,
@Nonnull final List<URL> urls,
@Nonnull final List<String> urlTexts) {
this.title = title;
this.content = content;
this.urls = urls;
this.urlTexts = urlTexts;
}
public MetaInfo() {
}
/**
* @return Title of the info. Can be empty.
*/
@Nonnull
public String getTitle() {
return title;
}
public void setTitle(@Nonnull final String title) {
this.title = title;
}
@Nonnull
public Description getContent() {
return content;
}
public void setContent(@Nonnull final Description content) {
this.content = content;
}
@Nonnull
public List<URL> getUrls() {
return urls;
}
public void setUrls(@Nonnull final List<URL> urls) {
this.urls = urls;
}
public void addUrl(@Nonnull final URL url) {
urls.add(url);
}
@Nonnull
public List<String> getUrlTexts() {
return urlTexts;
}
public void setUrlTexts(@Nonnull final List<String> urlTexts) {
this.urlTexts = urlTexts;
}
public void addUrlText(@Nonnull final String urlText) {
urlTexts.add(urlText);
}
}

View File

@ -0,0 +1,91 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* InfoItemsSearchCollector.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* A collector that can handle many extractor types, to be used when a list contains items of
* different types (e.g. search)
* <p>
* This collector can handle the following extractor types:
* <ul>
* <li>{@link StreamInfoItemExtractor}</li>
* <li>{@link ChannelInfoItemExtractor}</li>
* <li>{@link PlaylistInfoItemExtractor}</li>
* </ul>
* Calling {@link #extract(InfoItemExtractor)} or {@link #commit(InfoItemExtractor)} with any
* other extractor type will raise an exception.
*/
public class MultiInfoItemsCollector extends InfoItemsCollector<InfoItem, InfoItemExtractor> {
private final StreamInfoItemsCollector streamCollector;
private final ChannelInfoItemsCollector userCollector;
private final PlaylistInfoItemsCollector playlistCollector;
public MultiInfoItemsCollector(final int serviceId) {
super(serviceId);
streamCollector = new StreamInfoItemsCollector(serviceId);
userCollector = new ChannelInfoItemsCollector(serviceId);
playlistCollector = new PlaylistInfoItemsCollector(serviceId);
}
@Override
public List<Throwable> getErrors() {
final List<Throwable> errors = new ArrayList<>(super.getErrors());
errors.addAll(streamCollector.getErrors());
errors.addAll(userCollector.getErrors());
errors.addAll(playlistCollector.getErrors());
return Collections.unmodifiableList(errors);
}
@Override
public void reset() {
super.reset();
streamCollector.reset();
userCollector.reset();
playlistCollector.reset();
}
@Override
public InfoItem extract(final InfoItemExtractor extractor) throws ParsingException {
// Use the corresponding collector for each item extractor type
if (extractor instanceof StreamInfoItemExtractor) {
return streamCollector.extract((StreamInfoItemExtractor) extractor);
} else if (extractor instanceof ChannelInfoItemExtractor) {
return userCollector.extract((ChannelInfoItemExtractor) extractor);
} else if (extractor instanceof PlaylistInfoItemExtractor) {
return playlistCollector.extract((PlaylistInfoItemExtractor) extractor);
} else {
throw new IllegalArgumentException("Invalid extractor type: " + extractor);
}
}
}

View File

@ -0,0 +1,135 @@
package org.schabi.newpipe.extractor;
/*
* Created by Christian Schabesberger on 23.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* NewPipe.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Provides access to streaming services supported by NewPipe.
*/
public final class NewPipe {
private static Downloader downloader;
private static Localization preferredLocalization;
private static ContentCountry preferredContentCountry;
private NewPipe() {
}
public static void init(final Downloader d) {
init(d, Localization.DEFAULT);
}
public static void init(final Downloader d, final Localization l) {
init(d, l, l.getCountryCode().isEmpty()
? ContentCountry.DEFAULT : new ContentCountry(l.getCountryCode()));
}
public static void init(final Downloader d, final Localization l, final ContentCountry c) {
downloader = d;
preferredLocalization = l;
preferredContentCountry = c;
}
public static Downloader getDownloader() {
return downloader;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
public static List<StreamingService> getServices() {
return ServiceList.all();
}
public static StreamingService getService(final int serviceId) throws ExtractionException {
return ServiceList.all().stream()
.filter(service -> service.getServiceId() == serviceId)
.findFirst()
.orElseThrow(() -> new ExtractionException(
"There's no service with the id = \"" + serviceId + "\""));
}
public static StreamingService getService(final String serviceName) throws ExtractionException {
return ServiceList.all().stream()
.filter(service -> service.getServiceInfo().getName().equals(serviceName))
.findFirst()
.orElseThrow(() -> new ExtractionException(
"There's no service with the name = \"" + serviceName + "\""));
}
public static StreamingService getServiceByUrl(final String url) throws ExtractionException {
for (final StreamingService service : ServiceList.all()) {
if (service.getLinkTypeByUrl(url) != StreamingService.LinkType.NONE) {
return service;
}
}
throw new ExtractionException("No service can handle the url = \"" + url + "\"");
}
/*//////////////////////////////////////////////////////////////////////////
// Localization
//////////////////////////////////////////////////////////////////////////*/
public static void setupLocalization(final Localization thePreferredLocalization) {
setupLocalization(thePreferredLocalization, null);
}
public static void setupLocalization(
final Localization thePreferredLocalization,
@Nullable final ContentCountry thePreferredContentCountry) {
NewPipe.preferredLocalization = thePreferredLocalization;
if (thePreferredContentCountry != null) {
NewPipe.preferredContentCountry = thePreferredContentCountry;
} else {
NewPipe.preferredContentCountry = thePreferredLocalization.getCountryCode().isEmpty()
? ContentCountry.DEFAULT
: new ContentCountry(thePreferredLocalization.getCountryCode());
}
}
@Nonnull
public static Localization getPreferredLocalization() {
return preferredLocalization == null ? Localization.DEFAULT : preferredLocalization;
}
public static void setPreferredLocalization(final Localization preferredLocalization) {
NewPipe.preferredLocalization = preferredLocalization;
}
@Nonnull
public static ContentCountry getPreferredContentCountry() {
return preferredContentCountry == null ? ContentCountry.DEFAULT : preferredContentCountry;
}
public static void setPreferredContentCountry(final ContentCountry preferredContentCountry) {
NewPipe.preferredContentCountry = preferredContentCountry;
}
}

View File

@ -0,0 +1,81 @@
package org.schabi.newpipe.extractor;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class Page implements Serializable {
private final String url;
private final String id;
private final List<String> ids;
private final Map<String, String> cookies;
@Nullable
private final byte[] body;
public Page(final String url,
final String id,
final List<String> ids,
final Map<String, String> cookies,
@Nullable final byte[] body) {
this.url = url;
this.id = id;
this.ids = ids;
this.cookies = cookies;
this.body = body;
}
public Page(final String url) {
this(url, null, null, null, null);
}
public Page(final String url, final String id) {
this(url, id, null, null, null);
}
public Page(final String url, final byte[] body) {
this(url, null, null, null, body);
}
public Page(final String url, final Map<String, String> cookies) {
this(url, null, null, cookies, null);
}
public Page(final List<String> ids) {
this(null, null, ids, null, null);
}
public Page(final List<String> ids, final Map<String, String> cookies) {
this(null, null, ids, cookies, null);
}
public String getUrl() {
return url;
}
public String getId() {
return id;
}
public List<String> getIds() {
return ids;
}
public Map<String, String> getCookies() {
return cookies;
}
public static boolean isValid(final Page page) {
return page != null && (!isNullOrEmpty(page.getUrl())
|| !isNullOrEmpty(page.getIds()));
}
@Nullable
public byte[] getBody() {
return body;
}
}

View File

@ -0,0 +1,70 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.services.bandcamp.BandcampService;
import org.schabi.newpipe.extractor.services.media_ccc.MediaCCCService;
import org.schabi.newpipe.extractor.services.peertube.PeertubeService;
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudService;
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
import org.schabi.newpipe.extractor.services.xh.XhService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/*
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
* ServiceList.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* A list of supported services.
*/
@SuppressWarnings({"ConstantName", "InnerAssignment"}) // keep unusual names and inner assignments
public final class ServiceList {
private ServiceList() {
//no instance
}
public static final YoutubeService YouTube;
public static final SoundcloudService SoundCloud;
public static final MediaCCCService MediaCCC;
public static final PeertubeService PeerTube;
public static final BandcampService Bandcamp;
public static final XhService Xh;
/**
* When creating a new service, put this service in the end of this list,
* and give it the next free id.
*/
private static final List<StreamingService> SERVICES = Collections.unmodifiableList(
Arrays.asList(
YouTube = new YoutubeService(0),
SoundCloud = new SoundcloudService(1),
Bandcamp = new BandcampService(2),
MediaCCC = new MediaCCCService(3),
PeerTube = new PeertubeService(4),
Xh = new XhService(5)
));
/**
* Get all the supported services.
*
* @return a unmodifiable list of all the supported services
*/
public static List<StreamingService> all() {
return SERVICES;
}
}

View File

@ -0,0 +1,404 @@
package org.schabi.newpipe.extractor;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.feed.FeedExtractor;
import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.extractor.utils.Utils;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
/*
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
* StreamingService.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class StreamingService {
/**
* This class holds meta information about the service implementation.
*/
public static class ServiceInfo {
private final String name;
private final List<MediaCapability> mediaCapabilities;
/**
* Creates a new instance of a ServiceInfo
* @param name the name of the service
* @param mediaCapabilities the type of media this service can handle
*/
public ServiceInfo(final String name, final List<MediaCapability> mediaCapabilities) {
this.name = name;
this.mediaCapabilities = Collections.unmodifiableList(mediaCapabilities);
}
public String getName() {
return name;
}
public List<MediaCapability> getMediaCapabilities() {
return mediaCapabilities;
}
public enum MediaCapability {
AUDIO, VIDEO, LIVE, COMMENTS
}
}
/**
* LinkType will be used to determine which type of URL you are handling, and therefore which
* part of NewPipe should handle a certain URL.
*/
public enum LinkType {
NONE,
STREAM,
CHANNEL,
PLAYLIST
}
private final int serviceId;
private final ServiceInfo serviceInfo;
/**
* Creates a new Streaming service.
* If you Implement one do not set id within your implementation of this extractor, instead
* set the id when you put the extractor into {@link ServiceList}
* All other parameters can be set directly from the overriding constructor.
* @param id the number of the service to identify him within the NewPipe frontend
* @param name the name of the service
* @param capabilities the type of media this service can handle
*/
public StreamingService(final int id,
final String name,
final List<ServiceInfo.MediaCapability> capabilities) {
this.serviceId = id;
this.serviceInfo = new ServiceInfo(name, capabilities);
}
public final int getServiceId() {
return serviceId;
}
public ServiceInfo getServiceInfo() {
return serviceInfo;
}
@Override
public String toString() {
return serviceId + ":" + serviceInfo.getName();
}
public abstract String getBaseUrl();
/*//////////////////////////////////////////////////////////////////////////
// Url Id handler
//////////////////////////////////////////////////////////////////////////*/
/**
* Must return a new instance of an implementation of LinkHandlerFactory for streams.
* @return an instance of a LinkHandlerFactory for streams
*/
public abstract LinkHandlerFactory getStreamLHFactory();
/**
* Must return a new instance of an implementation of ListLinkHandlerFactory for channels.
* If support for channels is not given null must be returned.
* @return an instance of a ListLinkHandlerFactory for channels or null
*/
public abstract ListLinkHandlerFactory getChannelLHFactory();
/**
* Must return a new instance of an implementation of ListLinkHandlerFactory for playlists.
* If support for playlists is not given null must be returned.
* @return an instance of a ListLinkHandlerFactory for playlists or null
*/
public abstract ListLinkHandlerFactory getPlaylistLHFactory();
/**
* Must return an instance of an implementation of SearchQueryHandlerFactory.
* @return an instance of a SearchQueryHandlerFactory
*/
public abstract SearchQueryHandlerFactory getSearchQHFactory();
public abstract ListLinkHandlerFactory getCommentsLHFactory();
/*//////////////////////////////////////////////////////////////////////////
// Extractors
//////////////////////////////////////////////////////////////////////////*/
/**
* Must create a new instance of a SearchExtractor implementation.
* @param queryHandler specifies the keyword lock for, and the filters which should be applied.
* @return a new SearchExtractor instance
*/
public abstract SearchExtractor getSearchExtractor(SearchQueryHandler queryHandler);
/**
* Must create a new instance of a SuggestionExtractor implementation.
* @return a new SuggestionExtractor instance
*/
public abstract SuggestionExtractor getSuggestionExtractor();
/**
* Outdated or obsolete. null can be returned.
* @return just null
*/
public abstract SubscriptionExtractor getSubscriptionExtractor();
/**
* This method decides which strategy will be chosen to fetch the feed. In YouTube, for example,
* a separate feed exists which is lightweight and made specifically to be used like this.
* <p>
* In services which there's no other way to retrieve them, null should be returned.
*
* @return a {@link FeedExtractor} instance or null.
*/
@Nullable
public FeedExtractor getFeedExtractor(final String url) throws ExtractionException {
return null;
}
/**
* Must create a new instance of a KioskList implementation.
* @return a new KioskList instance
*/
public abstract KioskList getKioskList() throws ExtractionException;
/**
* Must create a new instance of a ChannelExtractor implementation.
* @param linkHandler is pointing to the channel which should be handled by this new instance.
* @return a new ChannelExtractor
*/
public abstract ChannelExtractor getChannelExtractor(ListLinkHandler linkHandler)
throws ExtractionException;
/**
* Must crete a new instance of a PlaylistExtractor implementation.
* @param linkHandler is pointing to the playlist which should be handled by this new instance.
* @return a new PlaylistExtractor
*/
public abstract PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler)
throws ExtractionException;
/**
* Must create a new instance of a StreamExtractor implementation.
* @param linkHandler is pointing to the stream which should be handled by this new instance.
* @return a new StreamExtractor
*/
public abstract StreamExtractor getStreamExtractor(LinkHandler linkHandler)
throws ExtractionException;
public abstract CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler)
throws ExtractionException;
/*//////////////////////////////////////////////////////////////////////////
// Extractors without link handler
//////////////////////////////////////////////////////////////////////////*/
public SearchExtractor getSearchExtractor(final String query,
final List<String> contentFilter,
final String sortFilter) throws ExtractionException {
return getSearchExtractor(getSearchQHFactory()
.fromQuery(query, contentFilter, sortFilter));
}
public ChannelExtractor getChannelExtractor(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ExtractionException {
return getChannelExtractor(getChannelLHFactory()
.fromQuery(id, contentFilter, sortFilter));
}
public PlaylistExtractor getPlaylistExtractor(final String id,
final List<String> contentFilter,
final String sortFilter)
throws ExtractionException {
return getPlaylistExtractor(getPlaylistLHFactory()
.fromQuery(id, contentFilter, sortFilter));
}
/*//////////////////////////////////////////////////////////////////////////
// Short extractors overloads
//////////////////////////////////////////////////////////////////////////*/
public SearchExtractor getSearchExtractor(final String query) throws ExtractionException {
return getSearchExtractor(getSearchQHFactory().fromQuery(query));
}
public ChannelExtractor getChannelExtractor(final String url) throws ExtractionException {
return getChannelExtractor(getChannelLHFactory().fromUrl(url));
}
public PlaylistExtractor getPlaylistExtractor(final String url) throws ExtractionException {
return getPlaylistExtractor(getPlaylistLHFactory().fromUrl(url));
}
public StreamExtractor getStreamExtractor(final String url) throws ExtractionException {
return getStreamExtractor(getStreamLHFactory().fromUrl(url));
}
public CommentsExtractor getCommentsExtractor(final String url) throws ExtractionException {
final ListLinkHandlerFactory listLinkHandlerFactory = getCommentsLHFactory();
if (listLinkHandlerFactory == null) {
return null;
}
return getCommentsExtractor(listLinkHandlerFactory.fromUrl(url));
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* Figures out where the link is pointing to (a channel, a video, a playlist, etc.)
* @param url the url on which it should be decided of which link type it is
* @return the link type of url
*/
public final LinkType getLinkTypeByUrl(final String url) throws ParsingException {
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
final LinkHandlerFactory sH = getStreamLHFactory();
final LinkHandlerFactory cH = getChannelLHFactory();
final LinkHandlerFactory pH = getPlaylistLHFactory();
if (sH != null && sH.acceptUrl(polishedUrl)) {
return LinkType.STREAM;
} else if (cH != null && cH.acceptUrl(polishedUrl)) {
return LinkType.CHANNEL;
} else if (pH != null && pH.acceptUrl(polishedUrl)) {
return LinkType.PLAYLIST;
} else {
return LinkType.NONE;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Localization
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns a list of localizations that this service supports.
*/
public List<Localization> getSupportedLocalizations() {
return Collections.singletonList(Localization.DEFAULT);
}
/**
* Returns a list of countries that this service supports.<br>
*/
public List<ContentCountry> getSupportedCountries() {
return Collections.singletonList(ContentCountry.DEFAULT);
}
/**
* Returns the localization that should be used in this service. It will get which localization
* the user prefer (using {@link NewPipe#getPreferredLocalization()}), then it will:
* <ul>
* <li>Check if the exactly localization is supported by this service.</li>
* <li>If not, check if a less specific localization is available, using only the language
* code.</li>
* <li>Fallback to the {@link Localization#DEFAULT default} localization.</li>
* </ul>
*/
public Localization getLocalization() {
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
// Check the localization's language and country
if (getSupportedLocalizations().contains(preferredLocalization)) {
return preferredLocalization;
}
// Fallback to the first supported language that matches the preferred language
for (final Localization supportedLanguage : getSupportedLocalizations()) {
if (supportedLanguage.getLanguageCode()
.equals(preferredLocalization.getLanguageCode())) {
return supportedLanguage;
}
}
return Localization.DEFAULT;
}
/**
* Returns the country that should be used to fetch content in this service. It will get which
* country the user prefer (using {@link NewPipe#getPreferredContentCountry()}), then it will:
* <ul>
* <li>Check if the country is supported by this service.</li>
* <li>If not, fallback to the {@link ContentCountry#DEFAULT default} country.</li>
* </ul>
*/
public ContentCountry getContentCountry() {
final ContentCountry preferredContentCountry = NewPipe.getPreferredContentCountry();
if (getSupportedCountries().contains(preferredContentCountry)) {
return preferredContentCountry;
}
return ContentCountry.DEFAULT;
}
/**
* Get an instance of the time ago parser using the patterns related to the passed localization.
* <br><br>
* Just like {@link #getLocalization()}, it will also try to fallback to a less specific
* localization if the exact one is not available/supported.
*
* @throws IllegalArgumentException if the localization is not supported (parsing patterns are
* not present).
*/
public TimeAgoParser getTimeAgoParser(final Localization localization) {
final TimeAgoParser targetParser = TimeAgoPatternsManager.getTimeAgoParserFor(localization);
if (targetParser != null) {
return targetParser;
}
if (!localization.getCountryCode().isEmpty()) {
final Localization lessSpecificLocalization
= new Localization(localization.getLanguageCode());
final TimeAgoParser lessSpecificParser
= TimeAgoPatternsManager.getTimeAgoParserFor(lessSpecificLocalization);
if (lessSpecificParser != null) {
return lessSpecificParser;
}
}
throw new IllegalArgumentException(
"Localization is not supported (\"" + localization + "\")");
}
}

View File

@ -0,0 +1,47 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
/*
* Created by Christian Schabesberger on 25.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class ChannelExtractor extends ListExtractor<StreamInfoItem> {
public static final long UNKNOWN_SUBSCRIBER_COUNT = -1;
public ChannelExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
public abstract String getAvatarUrl() throws ParsingException;
public abstract String getBannerUrl() throws ParsingException;
public abstract String getFeedUrl() throws ParsingException;
public abstract long getSubscriberCount() throws ParsingException;
public abstract String getDescription() throws ParsingException;
public abstract String getParentChannelName() throws ParsingException;
public abstract String getParentChannelUrl() throws ParsingException;
public abstract String getParentChannelAvatarUrl() throws ParsingException;
public abstract boolean isVerified() throws ParsingException;
}

View File

@ -0,0 +1,226 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
/*
* Created by Christian Schabesberger on 31.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfo extends ListInfo<StreamInfoItem> {
public ChannelInfo(final int serviceId,
final String id,
final String url,
final String originalUrl,
final String name,
final ListLinkHandler listLinkHandler) {
super(serviceId, id, url, originalUrl, name, listLinkHandler.getContentFilters(),
listLinkHandler.getSortFilter());
}
public static ChannelInfo getInfo(final String url) throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}
public static ChannelInfo getInfo(final StreamingService service, final String url)
throws IOException, ExtractionException {
final ChannelExtractor extractor = service.getChannelExtractor(url);
extractor.fetchPage();
return getInfo(extractor);
}
public static InfoItemsPage<StreamInfoItem> getMoreItems(final StreamingService service,
final String url,
final Page page)
throws IOException, ExtractionException {
return service.getChannelExtractor(url).getPage(page);
}
public static ChannelInfo getInfo(final ChannelExtractor extractor)
throws IOException, ExtractionException {
final int serviceId = extractor.getServiceId();
final String id = extractor.getId();
final String url = extractor.getUrl();
final String originalUrl = extractor.getOriginalUrl();
final String name = extractor.getName();
final ChannelInfo info =
new ChannelInfo(serviceId, id, url, originalUrl, name, extractor.getLinkHandler());
try {
info.setAvatarUrl(extractor.getAvatarUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setBannerUrl(extractor.getBannerUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setFeedUrl(extractor.getFeedUrl());
} catch (final Exception e) {
info.addError(e);
}
final InfoItemsPage<StreamInfoItem> itemsPage =
ExtractorHelper.getItemsPageOrLogError(info, extractor);
info.setRelatedItems(itemsPage.getItems());
info.setNextPage(itemsPage.getNextPage());
try {
info.setSubscriberCount(extractor.getSubscriberCount());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setDescription(extractor.getDescription());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setParentChannelName(extractor.getParentChannelName());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setParentChannelUrl(extractor.getParentChannelUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setParentChannelAvatarUrl(extractor.getParentChannelAvatarUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setVerified(extractor.isVerified());
} catch (final Exception e) {
info.addError(e);
}
return info;
}
private String avatarUrl;
private String parentChannelName;
private String parentChannelUrl;
private String parentChannelAvatarUrl;
private String bannerUrl;
private String feedUrl;
private long subscriberCount = -1;
private String description;
private String[] donationLinks;
private boolean verified;
public String getParentChannelName() {
return parentChannelName;
}
public void setParentChannelName(final String parentChannelName) {
this.parentChannelName = parentChannelName;
}
public String getParentChannelUrl() {
return parentChannelUrl;
}
public void setParentChannelUrl(final String parentChannelUrl) {
this.parentChannelUrl = parentChannelUrl;
}
public String getParentChannelAvatarUrl() {
return parentChannelAvatarUrl;
}
public void setParentChannelAvatarUrl(final String parentChannelAvatarUrl) {
this.parentChannelAvatarUrl = parentChannelAvatarUrl;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(final String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getBannerUrl() {
return bannerUrl;
}
public void setBannerUrl(final String bannerUrl) {
this.bannerUrl = bannerUrl;
}
public String getFeedUrl() {
return feedUrl;
}
public void setFeedUrl(final String feedUrl) {
this.feedUrl = feedUrl;
}
public long getSubscriberCount() {
return subscriberCount;
}
public void setSubscriberCount(final long subscriberCount) {
this.subscriberCount = subscriberCount;
}
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
public String[] getDonationLinks() {
return donationLinks;
}
public void setDonationLinks(final String[] donationLinks) {
this.donationLinks = donationLinks;
}
public boolean isVerified() {
return verified;
}
public void setVerified(final boolean verified) {
this.verified = verified;
}
}

View File

@ -0,0 +1,67 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.InfoItem;
/*
* Created by Christian Schabesberger on 11.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* ChannelInfoItem.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfoItem extends InfoItem {
private String description;
private long subscriberCount = -1;
private long streamCount = -1;
private boolean verified = false;
public ChannelInfoItem(final int serviceId, final String url, final String name) {
super(InfoType.CHANNEL, serviceId, url, name);
}
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
public long getSubscriberCount() {
return subscriberCount;
}
public void setSubscriberCount(final long subscriberCount) {
this.subscriberCount = subscriberCount;
}
public long getStreamCount() {
return streamCount;
}
public void setStreamCount(final long streamCount) {
this.streamCount = streamCount;
}
public boolean isVerified() {
return verified;
}
public void setVerified(final boolean verified) {
this.verified = verified;
}
}

View File

@ -0,0 +1,34 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.InfoItemExtractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* ChannelInfoItemExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public interface ChannelInfoItemExtractor extends InfoItemExtractor {
String getDescription() throws ParsingException;
long getSubscriberCount() throws ParsingException;
long getStreamCount() throws ParsingException;
boolean isVerified() throws ParsingException;
}

View File

@ -0,0 +1,67 @@
package org.schabi.newpipe.extractor.channel;
import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* ChannelInfoItemsCollector.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public final class ChannelInfoItemsCollector
extends InfoItemsCollector<ChannelInfoItem, ChannelInfoItemExtractor> {
public ChannelInfoItemsCollector(final int serviceId) {
super(serviceId);
}
@Override
public ChannelInfoItem extract(final ChannelInfoItemExtractor extractor)
throws ParsingException {
final ChannelInfoItem resultItem = new ChannelInfoItem(
getServiceId(), extractor.getUrl(), extractor.getName());
// optional information
try {
resultItem.setSubscriberCount(extractor.getSubscriberCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setStreamCount(extractor.getStreamCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setDescription(extractor.getDescription());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setVerified(extractor.isVerified());
} catch (final Exception e) {
addError(e);
}
return resultItem;
}
}

View File

@ -0,0 +1,37 @@
package org.schabi.newpipe.extractor.comments;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import javax.annotation.Nonnull;
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
public CommentsExtractor(final StreamingService service, final ListLinkHandler uiHandler) {
super(service, uiHandler);
}
/**
* @apiNote Warning: This method is experimental and may get removed in a future release.
* @return <code>true</code> if the comments are disabled otherwise <code>false</code> (default)
*/
public boolean isCommentsDisabled() throws ExtractionException {
return false;
}
/**
* @return the total number of comments
*/
public int getCommentsCount() throws ExtractionException {
return -1;
}
@Nonnull
@Override
public String getName() throws ParsingException {
return "Comments";
}
}

View File

@ -0,0 +1,126 @@
package org.schabi.newpipe.extractor.comments;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
private CommentsInfo(
final int serviceId,
final ListLinkHandler listUrlIdHandler,
final String name) {
super(serviceId, listUrlIdHandler, name);
}
public static CommentsInfo getInfo(final String url) throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}
public static CommentsInfo getInfo(final StreamingService service, final String url)
throws ExtractionException, IOException {
return getInfo(service.getCommentsExtractor(url));
}
public static CommentsInfo getInfo(final CommentsExtractor commentsExtractor)
throws IOException, ExtractionException {
// for services which do not have a comments extractor
if (commentsExtractor == null) {
return null;
}
commentsExtractor.fetchPage();
final String name = commentsExtractor.getName();
final int serviceId = commentsExtractor.getServiceId();
final ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
final CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
commentsInfo.setCommentsExtractor(commentsExtractor);
final InfoItemsPage<CommentsInfoItem> initialCommentsPage =
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
try {
commentsInfo.setCommentsCount(commentsExtractor.getCommentsCount());
} catch (final Exception e) {
commentsInfo.addError(e);
}
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
return commentsInfo;
}
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
final CommentsInfo commentsInfo,
final Page page) throws ExtractionException, IOException {
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo.getUrl(),
page);
}
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
final StreamingService service,
final CommentsInfo commentsInfo,
final Page page) throws IOException, ExtractionException {
return getMoreItems(service, commentsInfo.getUrl(), page);
}
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
final StreamingService service,
final String url,
final Page page) throws IOException, ExtractionException {
return service.getCommentsExtractor(url).getPage(page);
}
private transient CommentsExtractor commentsExtractor;
private boolean commentsDisabled = false;
private int commentsCount;
public CommentsExtractor getCommentsExtractor() {
return commentsExtractor;
}
public void setCommentsExtractor(final CommentsExtractor commentsExtractor) {
this.commentsExtractor = commentsExtractor;
}
/**
* @return {@code true} if the comments are disabled otherwise {@code false} (default)
* @see CommentsExtractor#isCommentsDisabled()
*/
public boolean isCommentsDisabled() {
return commentsDisabled;
}
/**
* @param commentsDisabled {@code true} if the comments are disabled otherwise {@code false}
*/
public void setCommentsDisabled(final boolean commentsDisabled) {
this.commentsDisabled = commentsDisabled;
}
/**
* Returns the total number of comments.
*
* @return the total number of comments
*/
public int getCommentsCount() {
return commentsCount;
}
/**
* Sets the total number of comments.
*
* @param commentsCount the commentsCount to set.
*/
public void setCommentsCount(final int commentsCount) {
this.commentsCount = commentsCount;
}
}

View File

@ -0,0 +1,170 @@
package org.schabi.newpipe.extractor.comments;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.Description;
import javax.annotation.Nullable;
public class CommentsInfoItem extends InfoItem {
private String commentId;
private Description commentText;
private String uploaderName;
private String uploaderAvatarUrl;
private String uploaderUrl;
private boolean uploaderVerified;
private String textualUploadDate;
@Nullable
private DateWrapper uploadDate;
private int likeCount;
private String textualLikeCount;
private boolean heartedByUploader;
private boolean pinned;
private int streamPosition;
private int replyCount;
@Nullable
private Page replies;
public static final int NO_LIKE_COUNT = -1;
public static final int NO_STREAM_POSITION = -1;
public static final int UNKNOWN_REPLY_COUNT = -1;
public CommentsInfoItem(final int serviceId, final String url, final String name) {
super(InfoType.COMMENT, serviceId, url, name);
}
public String getCommentId() {
return commentId;
}
public void setCommentId(final String commentId) {
this.commentId = commentId;
}
public Description getCommentText() {
return commentText;
}
public void setCommentText(final Description commentText) {
this.commentText = commentText;
}
public String getUploaderName() {
return uploaderName;
}
public void setUploaderName(final String uploaderName) {
this.uploaderName = uploaderName;
}
public String getUploaderAvatarUrl() {
return uploaderAvatarUrl;
}
public void setUploaderAvatarUrl(final String uploaderAvatarUrl) {
this.uploaderAvatarUrl = uploaderAvatarUrl;
}
public String getUploaderUrl() {
return uploaderUrl;
}
public void setUploaderUrl(final String uploaderUrl) {
this.uploaderUrl = uploaderUrl;
}
public String getTextualUploadDate() {
return textualUploadDate;
}
public void setTextualUploadDate(final String textualUploadDate) {
this.textualUploadDate = textualUploadDate;
}
@Nullable
public DateWrapper getUploadDate() {
return uploadDate;
}
public void setUploadDate(@Nullable final DateWrapper uploadDate) {
this.uploadDate = uploadDate;
}
/**
* @return the comment's like count
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable
*/
public int getLikeCount() {
return likeCount;
}
public void setLikeCount(final int likeCount) {
this.likeCount = likeCount;
}
public String getTextualLikeCount() {
return textualLikeCount;
}
public void setTextualLikeCount(final String textualLikeCount) {
this.textualLikeCount = textualLikeCount;
}
public void setHeartedByUploader(final boolean isHeartedByUploader) {
this.heartedByUploader = isHeartedByUploader;
}
public boolean isHeartedByUploader() {
return this.heartedByUploader;
}
public boolean isPinned() {
return pinned;
}
public void setPinned(final boolean pinned) {
this.pinned = pinned;
}
public void setUploaderVerified(final boolean uploaderVerified) {
this.uploaderVerified = uploaderVerified;
}
public boolean isUploaderVerified() {
return uploaderVerified;
}
public void setStreamPosition(final int streamPosition) {
this.streamPosition = streamPosition;
}
/**
* Get the playback position of the stream to which this comment belongs.
* This is not supported by all services.
*
* @return the playback position in seconds or {@link #NO_STREAM_POSITION} if not available
*/
public int getStreamPosition() {
return streamPosition;
}
public void setReplyCount(final int replyCount) {
this.replyCount = replyCount;
}
public int getReplyCount() {
return replyCount;
}
public void setReplies(@Nullable final Page replies) {
this.replies = replies;
}
@Nullable
public Page getReplies() {
return this.replies;
}
}

View File

@ -0,0 +1,133 @@
package org.schabi.newpipe.extractor.comments;
import org.schabi.newpipe.extractor.InfoItemExtractor;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import javax.annotation.Nullable;
public interface CommentsInfoItemExtractor extends InfoItemExtractor {
/**
* Return the like count of the comment,
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable.
*
* <br>
* <p>
* NOTE: Currently only implemented for YT {@link
* YoutubeCommentsInfoItemExtractor#getLikeCount()}
* with limitations (only approximate like count is returned)
*
* @return the comment's like count
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable
* @see StreamExtractor#getLikeCount()
*/
default int getLikeCount() throws ParsingException {
return CommentsInfoItem.NO_LIKE_COUNT;
}
/**
* The unmodified like count given by the service
* <br>
* It may be language dependent
*/
default String getTextualLikeCount() throws ParsingException {
return "";
}
/**
* The text of the comment
*/
default Description getCommentText() throws ParsingException {
return Description.EMPTY_DESCRIPTION;
}
/**
* The upload date given by the service, unmodified
*
* @see StreamExtractor#getTextualUploadDate()
*/
default String getTextualUploadDate() throws ParsingException {
return "";
}
/**
* The upload date wrapped with DateWrapper class
*
* @see StreamExtractor#getUploadDate()
*/
@Nullable
default DateWrapper getUploadDate() throws ParsingException {
return null;
}
default String getCommentId() throws ParsingException {
return "";
}
default String getUploaderUrl() throws ParsingException {
return "";
}
default String getUploaderName() throws ParsingException {
return "";
}
default String getUploaderAvatarUrl() throws ParsingException {
return "";
}
/**
* Whether the comment has been hearted by the uploader
*/
default boolean isHeartedByUploader() throws ParsingException {
return false;
}
/**
* Whether the comment is pinned
*/
default boolean isPinned() throws ParsingException {
return false;
}
/**
* Whether the uploader is verified by the service
*/
default boolean isUploaderVerified() throws ParsingException {
return false;
}
/**
* The playback position of the stream to which this comment belongs.
*
* @see CommentsInfoItem#getStreamPosition()
*/
default int getStreamPosition() throws ParsingException {
return CommentsInfoItem.NO_STREAM_POSITION;
}
/**
* The count of comment replies.
*
* @return the count of the replies
* or {@link CommentsInfoItem#UNKNOWN_REPLY_COUNT} if replies are not supported
*/
default int getReplyCount() throws ParsingException {
return CommentsInfoItem.UNKNOWN_REPLY_COUNT;
}
/**
* The continuation page which is used to get comment replies from.
*
* @return the continuation Page for the replies, or null if replies are not supported
*/
@Nullable
default Page getReplies() throws ParsingException {
return null;
}
}

View File

@ -0,0 +1,119 @@
package org.schabi.newpipe.extractor.comments;
import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.ArrayList;
import java.util.List;
public final class CommentsInfoItemsCollector
extends InfoItemsCollector<CommentsInfoItem, CommentsInfoItemExtractor> {
public CommentsInfoItemsCollector(final int serviceId) {
super(serviceId);
}
@Override
public CommentsInfoItem extract(final CommentsInfoItemExtractor extractor)
throws ParsingException {
final CommentsInfoItem resultItem = new CommentsInfoItem(
getServiceId(), extractor.getUrl(), extractor.getName());
// optional information
try {
resultItem.setCommentId(extractor.getCommentId());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setCommentText(extractor.getCommentText());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploaderName(extractor.getUploaderName());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploaderUrl(extractor.getUploaderUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setTextualUploadDate(extractor.getTextualUploadDate());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploadDate(extractor.getUploadDate());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setLikeCount(extractor.getLikeCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setTextualLikeCount(extractor.getTextualLikeCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setHeartedByUploader(extractor.isHeartedByUploader());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setPinned(extractor.isPinned());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setStreamPosition(extractor.getStreamPosition());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setReplyCount(extractor.getReplyCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setReplies(extractor.getReplies());
} catch (final Exception e) {
addError(e);
}
return resultItem;
}
@Override
public void commit(final CommentsInfoItemExtractor extractor) {
try {
addItem(extract(extractor));
} catch (final Exception e) {
addError(e);
}
}
public List<CommentsInfoItem> getCommentsInfoItemList() {
return new ArrayList<>(super.getItems());
}
}

View File

@ -0,0 +1,246 @@
package org.schabi.newpipe.extractor.downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.Localization;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A base for downloader implementations that NewPipe will use
* to download needed resources during extraction.
*/
public abstract class Downloader {
/**
* Do a GET request to get the resource that the url is pointing to.<br>
* <br>
* This method calls {@link #get(String, Map, Localization)} with the default preferred
* localization. It should only be used when the resource that will be fetched won't be affected
* by the localization.
*
* @param url the URL that is pointing to the wanted resource
* @return the result of the GET request
*/
public Response get(final String url) throws IOException, ReCaptchaException {
return get(url, null, NewPipe.getPreferredLocalization());
}
/**
* Do a GET request to get the resource that the url is pointing to.<br>
* <br>
* It will set the {@code Accept-Language} header to the language of the localization parameter.
*
* @param url the URL that is pointing to the wanted resource
* @param localization the source of the value of the {@code Accept-Language} header
* @return the result of the GET request
*/
public Response get(final String url, final Localization localization)
throws IOException, ReCaptchaException {
return get(url, null, localization);
}
/**
* Do a GET request with the specified headers.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @return the result of the GET request
*/
public Response get(final String url, @Nullable final Map<String, List<String>> headers)
throws IOException, ReCaptchaException {
return get(url, headers, NewPipe.getPreferredLocalization());
}
/**
* Do a GET request with the specified headers.<br>
* <br>
* It will set the {@code Accept-Language} header to the language of the localization parameter.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param localization the source of the value of the {@code Accept-Language} header
* @return the result of the GET request
*/
public Response get(final String url,
@Nullable final Map<String, List<String>> headers,
final Localization localization)
throws IOException, ReCaptchaException {
return execute(Request.newBuilder()
.get(url)
.headers(headers)
.localization(localization)
.build());
}
/**
* Do a HEAD request.
*
* @param url the URL that is pointing to the wanted resource
* @return the result of the HEAD request
*/
public Response head(final String url) throws IOException, ReCaptchaException {
return head(url, null);
}
/**
* Do a HEAD request with the specified headers.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @return the result of the HEAD request
*/
public Response head(final String url, @Nullable final Map<String, List<String>> headers)
throws IOException, ReCaptchaException {
return execute(Request.newBuilder()
.head(url)
.headers(headers)
.build());
}
/**
* Do a POST request with the specified headers, sending the data array.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @return the result of the POST request
*/
public Response post(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend)
throws IOException, ReCaptchaException {
return post(url, headers, dataToSend, NewPipe.getPreferredLocalization());
}
/**
* Do a POST request with the specified headers, sending the data array.
* <br>
* It will set the {@code Accept-Language} header to the language of the localization parameter.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @param localization the source of the value of the {@code Accept-Language} header
* @return the result of the POST request
*/
public Response post(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend,
final Localization localization)
throws IOException, ReCaptchaException {
return execute(Request.newBuilder()
.post(url, dataToSend)
.headers(headers)
.localization(localization)
.build());
}
/**
* Convenient method to send a POST request using the specified value of the
* {@code Content-Type} header with a given {@link Localization}.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @param localization the source of the value of the {@code Accept-Language} header
* @param contentType the mime type of the body sent, which will be set as the value of the
* {@code Content-Type} header
* @return the result of the POST request
* @see #post(String, Map, byte[], Localization)
*/
public Response postWithContentType(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend,
final Localization localization,
final String contentType)
throws IOException, ReCaptchaException {
final Map<String, List<String>> actualHeaders = new HashMap<>();
if (headers != null) {
actualHeaders.putAll(headers);
}
actualHeaders.put("Content-Type", Collections.singletonList(contentType));
return post(url, actualHeaders, dataToSend, localization);
}
/**
* Convenient method to send a POST request using the specified value of the
* {@code Content-Type} header.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @param contentType the mime type of the body sent, which will be set as the value of the
* {@code Content-Type} header
* @return the result of the POST request
* @see #post(String, Map, byte[], Localization)
*/
public Response postWithContentType(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend,
final String contentType)
throws IOException, ReCaptchaException {
return postWithContentType(url, headers, dataToSend, NewPipe.getPreferredLocalization(),
contentType);
}
/**
* Convenient method to send a POST request the JSON mime type as the value of the
* {@code Content-Type} header with a given {@link Localization}.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @param localization the source of the value of the {@code Accept-Language} header
* @return the result of the POST request
* @see #post(String, Map, byte[], Localization)
*/
public Response postWithContentTypeJson(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend,
final Localization localization)
throws IOException, ReCaptchaException {
return postWithContentType(url, headers, dataToSend, localization, "application/json");
}
/**
* Convenient method to send a POST request the JSON mime type as the value of the
* {@code Content-Type} header.
*
* @param url the URL that is pointing to the wanted resource
* @param headers a list of headers that will be used in the request.
* Any default headers <b>should</b> be overridden by these.
* @param dataToSend byte array that will be sent when doing the request.
* @return the result of the POST request
* @see #post(String, Map, byte[], Localization)
*/
public Response postWithContentTypeJson(final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend)
throws IOException, ReCaptchaException {
return postWithContentTypeJson(url, headers, dataToSend,
NewPipe.getPreferredLocalization());
}
/**
* Do a request using the specified {@link Request} object.
*
* @return the result of the request
*/
public abstract Response execute(@Nonnull Request request)
throws IOException, ReCaptchaException;
}

View File

@ -0,0 +1,280 @@
package org.schabi.newpipe.extractor.downloader;
import org.schabi.newpipe.extractor.localization.Localization;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* An object that holds request information used when {@link Downloader#execute(Request) executing}
* a request.
*/
public class Request {
private final String httpMethod;
private final String url;
private final Map<String, List<String>> headers;
@Nullable
private final byte[] dataToSend;
@Nullable
private final Localization localization;
public Request(final String httpMethod,
final String url,
@Nullable final Map<String, List<String>> headers,
@Nullable final byte[] dataToSend,
@Nullable final Localization localization,
final boolean automaticLocalizationHeader) {
this.httpMethod = Objects.requireNonNull(httpMethod, "Request's httpMethod is null");
this.url = Objects.requireNonNull(url, "Request's url is null");
this.dataToSend = dataToSend;
this.localization = localization;
final Map<String, List<String>> actualHeaders = new LinkedHashMap<>();
if (headers != null) {
actualHeaders.putAll(headers);
}
if (automaticLocalizationHeader && localization != null) {
actualHeaders.putAll(getHeadersFromLocalization(localization));
}
this.headers = Collections.unmodifiableMap(actualHeaders);
}
private Request(final Builder builder) {
this(builder.httpMethod, builder.url, builder.headers, builder.dataToSend,
builder.localization, builder.automaticLocalizationHeader);
}
/**
* A http method (i.e. {@code GET, POST, HEAD}).
*/
public String httpMethod() {
return httpMethod;
}
/**
* The URL that is pointing to the wanted resource.
*/
public String url() {
return url;
}
/**
* A list of headers that will be used in the request.<br>
* Any default headers that the implementation may have, <b>should</b> be overridden by these.
*/
public Map<String, List<String>> headers() {
return headers;
}
/**
* An optional byte array that will be sent when doing the request, very commonly used in
* {@code POST} requests.<br>
* <br>
* The implementation should make note of some recommended headers
* (for example, {@code Content-Length} in a post request).
*/
@Nullable
public byte[] dataToSend() {
return dataToSend;
}
/**
* A localization object that should be used when executing a request.<br>
* <br>
* Usually the {@code Accept-Language} will be set to this value (a helper
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
*/
@Nullable
public Localization localization() {
return localization;
}
public static Builder newBuilder() {
return new Builder();
}
public static final class Builder {
private String httpMethod;
private String url;
private final Map<String, List<String>> headers = new LinkedHashMap<>();
private byte[] dataToSend;
private Localization localization;
private boolean automaticLocalizationHeader = true;
public Builder() {
}
/**
* A http method (i.e. {@code GET, POST, HEAD}).
*/
public Builder httpMethod(final String httpMethodToSet) {
this.httpMethod = httpMethodToSet;
return this;
}
/**
* The URL that is pointing to the wanted resource.
*/
public Builder url(final String urlToSet) {
this.url = urlToSet;
return this;
}
/**
* A list of headers that will be used in the request.<br>
* Any default headers that the implementation may have, <b>should</b> be overridden by
* these.
*/
public Builder headers(@Nullable final Map<String, List<String>> headersToSet) {
this.headers.clear();
if (headersToSet != null) {
this.headers.putAll(headersToSet);
}
return this;
}
/**
* An optional byte array that will be sent when doing the request, very commonly used in
* {@code POST} requests.<br>
* <br>
* The implementation should make note of some recommended headers
* (for example, {@code Content-Length} in a post request).
*/
public Builder dataToSend(final byte[] dataToSendToSet) {
this.dataToSend = dataToSendToSet;
return this;
}
/**
* A localization object that should be used when executing a request.<br>
* <br>
* Usually the {@code Accept-Language} will be set to this value (a helper
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
*/
public Builder localization(final Localization localizationToSet) {
this.localization = localizationToSet;
return this;
}
/**
* If localization headers should automatically be included in the request.
*/
public Builder automaticLocalizationHeader(final boolean automaticLocalizationHeaderToSet) {
this.automaticLocalizationHeader = automaticLocalizationHeaderToSet;
return this;
}
public Request build() {
return new Request(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Http Methods Utils
//////////////////////////////////////////////////////////////////////////*/
public Builder get(final String urlToSet) {
this.httpMethod = "GET";
this.url = urlToSet;
return this;
}
public Builder head(final String urlToSet) {
this.httpMethod = "HEAD";
this.url = urlToSet;
return this;
}
public Builder post(final String urlToSet, @Nullable final byte[] dataToSendToSet) {
this.httpMethod = "POST";
this.url = urlToSet;
this.dataToSend = dataToSendToSet;
return this;
}
/*//////////////////////////////////////////////////////////////////////////
// Additional Headers Utils
//////////////////////////////////////////////////////////////////////////*/
public Builder setHeaders(final String headerName, final List<String> headerValueList) {
this.headers.remove(headerName);
this.headers.put(headerName, headerValueList);
return this;
}
public Builder addHeaders(final String headerName, final List<String> headerValueList) {
@Nullable List<String> currentHeaderValueList = this.headers.get(headerName);
if (currentHeaderValueList == null) {
currentHeaderValueList = new ArrayList<>();
}
currentHeaderValueList.addAll(headerValueList);
this.headers.put(headerName, headerValueList);
return this;
}
public Builder setHeader(final String headerName, final String headerValue) {
return setHeaders(headerName, Collections.singletonList(headerValue));
}
public Builder addHeader(final String headerName, final String headerValue) {
return addHeaders(headerName, Collections.singletonList(headerValue));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@SuppressWarnings("WeakerAccess")
@Nonnull
public static Map<String, List<String>> getHeadersFromLocalization(
@Nullable final Localization localization) {
if (localization == null) {
return Collections.emptyMap();
}
final String languageCode = localization.getLanguageCode();
final List<String> languageCodeList = Collections.singletonList(
localization.getCountryCode().isEmpty() ? languageCode
: localization.getLocalizationCode() + ", " + languageCode + ";q=0.9");
return Collections.singletonMap("Accept-Language", languageCodeList);
}
/*//////////////////////////////////////////////////////////////////////////
// Generated
//////////////////////////////////////////////////////////////////////////*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Request request = (Request) o;
return httpMethod.equals(request.httpMethod)
&& url.equals(request.url)
&& headers.equals(request.headers)
&& Arrays.equals(dataToSend, request.dataToSend)
&& Objects.equals(localization, request.localization);
}
@Override
public int hashCode() {
int result = Objects.hash(httpMethod, url, headers, localization);
result = 31 * result + Arrays.hashCode(dataToSend);
return result;
}
}

View File

@ -0,0 +1,83 @@
package org.schabi.newpipe.extractor.downloader;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* A Data class used to hold the results from requests made by the Downloader implementation.
*/
public class Response {
private final int responseCode;
private final String responseMessage;
private final Map<String, List<String>> responseHeaders;
private final String responseBody;
private final String latestUrl;
public Response(final int responseCode,
final String responseMessage,
@Nullable final Map<String, List<String>> responseHeaders,
@Nullable final String responseBody,
@Nullable final String latestUrl) {
this.responseCode = responseCode;
this.responseMessage = responseMessage;
this.responseHeaders = responseHeaders == null ? Collections.emptyMap() : responseHeaders;
this.responseBody = responseBody == null ? "" : responseBody;
this.latestUrl = latestUrl;
}
public int responseCode() {
return responseCode;
}
public String responseMessage() {
return responseMessage;
}
public Map<String, List<String>> responseHeaders() {
return responseHeaders;
}
@Nonnull
public String responseBody() {
return responseBody;
}
/**
* Used for detecting a possible redirection, limited to the latest one.
*
* @return latest url known right before this response object was created
*/
@Nonnull
public String latestUrl() {
return latestUrl;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
/**
* For easy access to some header value that (usually) don't repeat itself.
* <p>For getting all the values associated to the header, use {@link #responseHeaders()} (e.g.
* {@code Set-Cookie}).
*
* @param name the name of the header
* @return the first value assigned to this header
*/
@Nullable
public String getHeader(final String name) {
for (final Map.Entry<String, List<String>> headerEntry : responseHeaders.entrySet()) {
final String key = headerEntry.getKey();
if (key != null && key.equalsIgnoreCase(name) && !headerEntry.getValue().isEmpty()) {
return headerEntry.getValue().get(0);
}
}
return null;
}
}

View File

@ -0,0 +1,31 @@
package org.schabi.newpipe.extractor.exceptions;
public class AccountTerminatedException extends ContentNotAvailableException {
private Reason reason = Reason.UNKNOWN;
public AccountTerminatedException(final String message) {
super(message);
}
public AccountTerminatedException(final String message, final Reason reason) {
super(message);
this.reason = reason;
}
public AccountTerminatedException(final String message, final Throwable cause) {
super(message, cause);
}
/**
* The reason for the violation. There should also be more info in the exception's message.
*/
public Reason getReason() {
return reason;
}
public enum Reason {
UNKNOWN,
VIOLATION
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class AgeRestrictedContentException extends ContentNotAvailableException {
public AgeRestrictedContentException(final String message) {
super(message);
}
public AgeRestrictedContentException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class ContentNotAvailableException extends ParsingException {
public ContentNotAvailableException(final String message) {
super(message);
}
public ContentNotAvailableException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class ContentNotSupportedException extends ParsingException {
public ContentNotSupportedException(final String message) {
super(message);
}
public ContentNotSupportedException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,35 @@
package org.schabi.newpipe.extractor.exceptions;
/*
* Created by Christian Schabesberger on 30.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ExtractionException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExtractionException extends Exception {
public ExtractionException(final String message) {
super(message);
}
public ExtractionException(final Throwable cause) {
super(cause);
}
public ExtractionException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,31 @@
package org.schabi.newpipe.extractor.exceptions;
/*
* Created by Christian Schabesberger on 12.09.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* FoundAdException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class FoundAdException extends ParsingException {
public FoundAdException(final String message) {
super(message);
}
public FoundAdException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class GeographicRestrictionException extends ContentNotAvailableException {
public GeographicRestrictionException(final String message) {
super(message);
}
public GeographicRestrictionException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class PaidContentException extends ContentNotAvailableException {
public PaidContentException(final String message) {
super(message);
}
public PaidContentException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,32 @@
package org.schabi.newpipe.extractor.exceptions;
/*
* Created by Christian Schabesberger on 31.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ParsingException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ParsingException extends ExtractionException {
public ParsingException(final String message) {
super(message);
}
public ParsingException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class PrivateContentException extends ContentNotAvailableException {
public PrivateContentException(final String message) {
super(message);
}
public PrivateContentException(final String message, final Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,34 @@
package org.schabi.newpipe.extractor.exceptions;
/*
* Created by beneth <bmauduit@beneth.fr> on 07.12.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ReCaptchaException.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ReCaptchaException extends ExtractionException {
private final String url;
public ReCaptchaException(final String message, final String url) {
super(message);
this.url = url;
}
public String getUrl() {
return url;
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class SoundCloudGoPlusContentException extends ContentNotAvailableException {
public SoundCloudGoPlusContentException() {
super("This track is a SoundCloud Go+ track");
}
public SoundCloudGoPlusContentException(final Throwable cause) {
super("This track is a SoundCloud Go+ track", cause);
}
}

View File

@ -0,0 +1,11 @@
package org.schabi.newpipe.extractor.exceptions;
public class YoutubeMusicPremiumContentException extends ContentNotAvailableException {
public YoutubeMusicPremiumContentException() {
super("This video is a YouTube Music Premium video");
}
public YoutubeMusicPremiumContentException(final Throwable cause) {
super("This video is a YouTube Music Premium video", cause);
}
}

View File

@ -0,0 +1,17 @@
package org.schabi.newpipe.extractor.feed;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
/**
* This class helps to extract items from lightweight feeds that the services may provide.
* <p>
* YouTube is an example of a service that has this alternative available.
*/
public abstract class FeedExtractor extends ListExtractor<StreamInfoItem> {
public FeedExtractor(final StreamingService service, final ListLinkHandler listLinkHandler) {
super(service, listLinkHandler);
}
}

View File

@ -0,0 +1,62 @@
package org.schabi.newpipe.extractor.feed;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
import java.util.List;
public class FeedInfo extends ListInfo<StreamInfoItem> {
public FeedInfo(final int serviceId,
final String id,
final String url,
final String originalUrl,
final String name,
final List<String> contentFilter,
final String sortFilter) {
super(serviceId, id, url, originalUrl, name, contentFilter, sortFilter);
}
public static FeedInfo getInfo(final String url) throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}
public static FeedInfo getInfo(final StreamingService service, final String url)
throws IOException, ExtractionException {
final FeedExtractor extractor = service.getFeedExtractor(url);
if (extractor == null) {
throw new IllegalArgumentException("Service \"" + service.getServiceInfo().getName()
+ "\" doesn't support FeedExtractor.");
}
extractor.fetchPage();
return getInfo(extractor);
}
public static FeedInfo getInfo(final FeedExtractor extractor)
throws IOException, ExtractionException {
extractor.fetchPage();
final int serviceId = extractor.getServiceId();
final String id = extractor.getId();
final String url = extractor.getUrl();
final String originalUrl = extractor.getOriginalUrl();
final String name = extractor.getName();
final FeedInfo info = new FeedInfo(serviceId, id, url, originalUrl, name, null, null);
final InfoItemsPage<StreamInfoItem> itemsPage
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
info.setRelatedItems(itemsPage.getItems());
info.setNextPage(itemsPage.getNextPage());
return info;
}
}

View File

@ -0,0 +1,57 @@
package org.schabi.newpipe.extractor.kiosk;
/*
* Created by Christian Schabesberger on 12.08.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* KioskExtractor.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import javax.annotation.Nonnull;
public abstract class KioskExtractor<T extends InfoItem> extends ListExtractor<T> {
private final String id;
public KioskExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler);
this.id = kioskId;
}
@Nonnull
@Override
public String getId() {
return id;
}
/**
* Id should be the name of the kiosk, tho Id is used for identifying it in the frontend,
* so id should be kept in english.
* In order to get the name of the kiosk in the desired language we have to
* crawl if from the website.
* @return the translated version of id
*/
@Nonnull
@Override
public abstract String getName() throws ParsingException;
}

View File

@ -0,0 +1,75 @@
package org.schabi.newpipe.extractor.kiosk;
/*
* Created by Christian Schabesberger on 12.08.17.
*
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
* KioskInfo.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
public final class KioskInfo extends ListInfo<StreamInfoItem> {
private KioskInfo(final int serviceId, final ListLinkHandler linkHandler, final String name) {
super(serviceId, linkHandler, name);
}
public static ListExtractor.InfoItemsPage<StreamInfoItem> getMoreItems(
final StreamingService service, final String url, final Page page)
throws IOException, ExtractionException {
return service.getKioskList().getExtractorByUrl(url, page).getPage(page);
}
public static KioskInfo getInfo(final String url) throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}
public static KioskInfo getInfo(final StreamingService service, final String url)
throws IOException, ExtractionException {
final KioskExtractor extractor = service.getKioskList().getExtractorByUrl(url, null);
extractor.fetchPage();
return getInfo(extractor);
}
/**
* Get KioskInfo from KioskExtractor
*
* @param extractor an extractor where fetchPage() was already got called on.
*/
public static KioskInfo getInfo(final KioskExtractor extractor) throws ExtractionException {
final KioskInfo info = new KioskInfo(extractor.getServiceId(),
extractor.getLinkHandler(),
extractor.getName());
final ListExtractor.InfoItemsPage<StreamInfoItem> itemsPage
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
info.setRelatedItems(itemsPage.getItems());
info.setNextPage(itemsPage.getNextPage());
return info;
}
}

View File

@ -0,0 +1,156 @@
package org.schabi.newpipe.extractor.kiosk;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
public class KioskList {
public interface KioskExtractorFactory {
KioskExtractor createNewKiosk(StreamingService streamingService,
String url,
String kioskId)
throws ExtractionException, IOException;
}
private final StreamingService service;
private final HashMap<String, KioskEntry> kioskList = new HashMap<>();
private String defaultKiosk = null;
@Nullable
private Localization forcedLocalization;
@Nullable
private ContentCountry forcedContentCountry;
private static class KioskEntry {
KioskEntry(final KioskExtractorFactory ef, final ListLinkHandlerFactory h) {
extractorFactory = ef;
handlerFactory = h;
}
final KioskExtractorFactory extractorFactory;
final ListLinkHandlerFactory handlerFactory;
}
public KioskList(final StreamingService service) {
this.service = service;
}
public void addKioskEntry(final KioskExtractorFactory extractorFactory,
final ListLinkHandlerFactory handlerFactory,
final String id)
throws Exception {
if (kioskList.get(id) != null) {
throw new Exception("Kiosk with type " + id + " already exists.");
}
kioskList.put(id, new KioskEntry(extractorFactory, handlerFactory));
}
public void setDefaultKiosk(final String kioskType) {
defaultKiosk = kioskType;
}
public KioskExtractor getDefaultKioskExtractor()
throws ExtractionException, IOException {
return getDefaultKioskExtractor(null);
}
public KioskExtractor getDefaultKioskExtractor(final Page nextPage)
throws ExtractionException, IOException {
return getDefaultKioskExtractor(nextPage, NewPipe.getPreferredLocalization());
}
public KioskExtractor getDefaultKioskExtractor(final Page nextPage,
final Localization localization)
throws ExtractionException, IOException {
if (!isNullOrEmpty(defaultKiosk)) {
return getExtractorById(defaultKiosk, nextPage, localization);
} else {
final String first = kioskList.keySet().stream().findAny().orElse(null);
if (first != null) {
// if not set get any entry
return getExtractorById(first, nextPage, localization);
} else {
return null;
}
}
}
public String getDefaultKioskId() {
return defaultKiosk;
}
public KioskExtractor getExtractorById(final String kioskId, final Page nextPage)
throws ExtractionException, IOException {
return getExtractorById(kioskId, nextPage, NewPipe.getPreferredLocalization());
}
public KioskExtractor getExtractorById(final String kioskId,
final Page nextPage,
final Localization localization)
throws ExtractionException, IOException {
final KioskEntry ke = kioskList.get(kioskId);
if (ke == null) {
throw new ExtractionException("No kiosk found with the type: " + kioskId);
} else {
final KioskExtractor kioskExtractor = ke.extractorFactory.createNewKiosk(service,
ke.handlerFactory.fromId(kioskId).getUrl(), kioskId);
if (forcedLocalization != null) {
kioskExtractor.forceLocalization(forcedLocalization);
}
if (forcedContentCountry != null) {
kioskExtractor.forceContentCountry(forcedContentCountry);
}
return kioskExtractor;
}
}
public Set<String> getAvailableKiosks() {
return kioskList.keySet();
}
public KioskExtractor getExtractorByUrl(final String url, final Page nextPage)
throws ExtractionException, IOException {
return getExtractorByUrl(url, nextPage, NewPipe.getPreferredLocalization());
}
public KioskExtractor getExtractorByUrl(final String url,
final Page nextPage,
final Localization localization)
throws ExtractionException, IOException {
for (final Map.Entry<String, KioskEntry> e : kioskList.entrySet()) {
final KioskEntry ke = e.getValue();
if (ke.handlerFactory.acceptUrl(url)) {
return getExtractorById(ke.handlerFactory.getId(url), nextPage, localization);
}
}
throw new ExtractionException("Could not find a kiosk that fits to the url: " + url);
}
public ListLinkHandlerFactory getListLinkHandlerFactoryByType(final String type) {
return kioskList.get(type).handlerFactory;
}
public void forceLocalization(@Nullable final Localization localization) {
this.forcedLocalization = localization;
}
public void forceContentCountry(@Nullable final ContentCountry contentCountry) {
this.forcedContentCountry = contentCountry;
}
}

View File

@ -0,0 +1,38 @@
package org.schabi.newpipe.extractor.linkhandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.Serializable;
public class LinkHandler implements Serializable {
protected final String originalUrl;
protected final String url;
protected final String id;
public LinkHandler(final String originalUrl, final String url, final String id) {
this.originalUrl = originalUrl;
this.url = url;
this.id = id;
}
public LinkHandler(final LinkHandler handler) {
this(handler.originalUrl, handler.url, handler.id);
}
public String getOriginalUrl() {
return originalUrl;
}
public String getUrl() {
return url;
}
public String getId() {
return id;
}
public String getBaseUrl() throws ParsingException {
return Utils.getBaseUrl(url);
}
}

View File

@ -0,0 +1,106 @@
package org.schabi.newpipe.extractor.linkhandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.Utils;
import java.util.Objects;
/*
* Created by Christian Schabesberger on 26.07.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* LinkHandlerFactory.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class LinkHandlerFactory {
///////////////////////////////////
// To Override
///////////////////////////////////
public abstract String getId(String url) throws ParsingException;
public abstract String getUrl(String id) throws ParsingException;
public abstract boolean onAcceptUrl(String url) throws ParsingException;
public String getUrl(final String id, final String baseUrl) throws ParsingException {
return getUrl(id);
}
///////////////////////////////////
// Logic
///////////////////////////////////
/**
* Builds a {@link LinkHandler} from a url.<br>
* Be sure to call {@link Utils#followGoogleRedirectIfNeeded(String)} on the url if overriding
* this function.
*
* @param url the url to extract path and id from
* @return a {@link LinkHandler} complete with information
*/
public LinkHandler fromUrl(final String url) throws ParsingException {
if (Utils.isNullOrEmpty(url)) {
throw new IllegalArgumentException("The url is null or empty");
}
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
final String baseUrl = Utils.getBaseUrl(polishedUrl);
return fromUrl(polishedUrl, baseUrl);
}
/**
* Builds a {@link LinkHandler} from an URL and a base URL. The URL is expected to be already
* polished from Google search redirects (otherwise how could {@code baseUrl} have been
* extracted?).<br>
* So do not call {@link Utils#followGoogleRedirectIfNeeded(String)} on the URL if overriding
* this function, since that should be done in {@link #fromUrl(String)}.
*
* @param url the URL without Google search redirects to extract id from
* @param baseUrl the base URL
* @return a {@link LinkHandler} complete with information
*/
public LinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
Objects.requireNonNull(url, "URL cannot be null");
if (!acceptUrl(url)) {
throw new ParsingException("URL not accepted: " + url);
}
final String id = getId(url);
return new LinkHandler(url, getUrl(id, baseUrl), id);
}
public LinkHandler fromId(final String id) throws ParsingException {
Objects.requireNonNull(id, "ID cannot be null");
final String url = getUrl(id);
return new LinkHandler(url, url, id);
}
public LinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
Objects.requireNonNull(id, "ID cannot be null");
final String url = getUrl(id, baseUrl);
return new LinkHandler(url, url, id);
}
/**
* When a VIEW_ACTION is caught this function will test if the url delivered within the calling
* Intent was meant to be watched with this Service.
* Return false if this service shall not allow to be called through ACTIONs.
*/
public boolean acceptUrl(final String url) throws ParsingException {
return onAcceptUrl(url);
}
}

View File

@ -0,0 +1,43 @@
package org.schabi.newpipe.extractor.linkhandler;
import java.util.Collections;
import java.util.List;
public class ListLinkHandler extends LinkHandler {
protected final List<String> contentFilters;
protected final String sortFilter;
public ListLinkHandler(final String originalUrl,
final String url,
final String id,
final List<String> contentFilters,
final String sortFilter) {
super(originalUrl, url, id);
this.contentFilters = Collections.unmodifiableList(contentFilters);
this.sortFilter = sortFilter;
}
public ListLinkHandler(final ListLinkHandler handler) {
this(handler.originalUrl,
handler.url,
handler.id,
handler.contentFilters,
handler.sortFilter);
}
public ListLinkHandler(final LinkHandler handler) {
this(handler.originalUrl,
handler.url,
handler.id,
Collections.emptyList(),
"");
}
public List<String> getContentFilters() {
return contentFilters;
}
public String getSortFilter() {
return sortFilter;
}
}

View File

@ -0,0 +1,103 @@
package org.schabi.newpipe.extractor.linkhandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
///////////////////////////////////
// To Override
///////////////////////////////////
public abstract String getUrl(String id, List<String> contentFilter, String sortFilter)
throws ParsingException;
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter,
final String baseUrl) throws ParsingException {
return getUrl(id, contentFilter, sortFilter);
}
///////////////////////////////////
// Logic
///////////////////////////////////
@Override
public ListLinkHandler fromUrl(final String url) throws ParsingException {
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
final String baseUrl = Utils.getBaseUrl(polishedUrl);
return fromUrl(polishedUrl, baseUrl);
}
@Override
public ListLinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
Objects.requireNonNull(url, "URL may not be null");
return new ListLinkHandler(super.fromUrl(url, baseUrl));
}
@Override
public ListLinkHandler fromId(final String id) throws ParsingException {
return new ListLinkHandler(super.fromId(id));
}
@Override
public ListLinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
return new ListLinkHandler(super.fromId(id, baseUrl));
}
public ListLinkHandler fromQuery(final String id,
final List<String> contentFilters,
final String sortFilter) throws ParsingException {
final String url = getUrl(id, contentFilters, sortFilter);
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
}
public ListLinkHandler fromQuery(final String id,
final List<String> contentFilters,
final String sortFilter,
final String baseUrl) throws ParsingException {
final String url = getUrl(id, contentFilters, sortFilter, baseUrl);
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
}
/**
* For making ListLinkHandlerFactory compatible with LinkHandlerFactory we need to override
* this, however it should not be overridden by the actual implementation.
*
* @return the url corresponding to id without any filters applied
*/
public String getUrl(final String id) throws ParsingException {
return getUrl(id, new ArrayList<>(0), "");
}
@Override
public String getUrl(final String id, final String baseUrl) throws ParsingException {
return getUrl(id, new ArrayList<>(0), "", baseUrl);
}
/**
* Will returns content filter the corresponding extractor can handle like "channels", "videos",
* "music", etc.
*
* @return filter that can be applied when building a query for getting a list
*/
public String[] getAvailableContentFilter() {
return new String[0];
}
/**
* Will returns sort filter the corresponding extractor can handle like "A-Z", "oldest first",
* "size", etc.
*
* @return filter that can be applied when building a query for getting a list
*/
public String[] getAvailableSortFilter() {
return new String[0];
}
}

View File

@ -0,0 +1,33 @@
package org.schabi.newpipe.extractor.linkhandler;
import java.util.List;
public class SearchQueryHandler extends ListLinkHandler {
public SearchQueryHandler(final String originalUrl,
final String url,
final String searchString,
final List<String> contentFilters,
final String sortFilter) {
super(originalUrl, url, searchString, contentFilters, sortFilter);
}
public SearchQueryHandler(final ListLinkHandler handler) {
this(handler.originalUrl,
handler.url,
handler.id,
handler.contentFilters,
handler.sortFilter);
}
/**
* Returns the search string. Since ListQIHandler is based on ListLinkHandler
* getSearchString() is equivalent to calling getId().
*
* @return the search string
*/
public String getSearchString() {
return getId();
}
}

View File

@ -0,0 +1,50 @@
package org.schabi.newpipe.extractor.linkhandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import java.util.Collections;
import java.util.List;
public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
///////////////////////////////////
// To Override
///////////////////////////////////
@Override
public abstract String getUrl(String query, List<String> contentFilter, String sortFilter)
throws ParsingException;
@SuppressWarnings("unused")
public String getSearchString(final String url) {
return "";
}
///////////////////////////////////
// Logic
///////////////////////////////////
@Override
public String getId(final String url) {
return getSearchString(url);
}
@Override
public SearchQueryHandler fromQuery(final String query,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
return new SearchQueryHandler(super.fromQuery(query, contentFilter, sortFilter));
}
public SearchQueryHandler fromQuery(final String query) throws ParsingException {
return fromQuery(query, Collections.emptyList(), "");
}
/**
* It's not mandatory for NewPipe to handle the Url
*/
@Override
public boolean onAcceptUrl(final String url) {
return false;
}
}

View File

@ -0,0 +1,64 @@
package org.schabi.newpipe.extractor.localization;
import javax.annotation.Nonnull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents a country that should be used when fetching content.
* <p>
* YouTube, for example, give different results in their feed depending on which country is
* selected.
* </p>
*/
public class ContentCountry implements Serializable {
public static final ContentCountry DEFAULT =
new ContentCountry(Localization.DEFAULT.getCountryCode());
@Nonnull
private final String countryCode;
public static List<ContentCountry> listFrom(final String... countryCodeList) {
final List<ContentCountry> toReturn = new ArrayList<>();
for (final String countryCode : countryCodeList) {
toReturn.add(new ContentCountry(countryCode));
}
return Collections.unmodifiableList(toReturn);
}
public ContentCountry(@Nonnull final String countryCode) {
this.countryCode = countryCode;
}
@Nonnull
public String getCountryCode() {
return countryCode;
}
@Override
public String toString() {
return getCountryCode();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ContentCountry)) {
return false;
}
final ContentCountry that = (ContentCountry) o;
return countryCode.equals(that.countryCode);
}
@Override
public int hashCode() {
return countryCode.hashCode();
}
}

View File

@ -0,0 +1,72 @@
package org.schabi.newpipe.extractor.localization;
import javax.annotation.Nonnull;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* A wrapper class that provides a field to describe if the date/time is precise or just an
* approximation.
*/
public class DateWrapper implements Serializable {
@Nonnull
private final OffsetDateTime offsetDateTime;
private final boolean isApproximation;
/**
* @deprecated Use {@link #DateWrapper(OffsetDateTime)} instead.
*/
@Deprecated
public DateWrapper(@Nonnull final Calendar calendar) {
//noinspection deprecation
this(calendar, false);
}
/**
* @deprecated Use {@link #DateWrapper(OffsetDateTime, boolean)} instead.
*/
@Deprecated
public DateWrapper(@Nonnull final Calendar calendar, final boolean isApproximation) {
this(OffsetDateTime.ofInstant(calendar.toInstant(), ZoneOffset.UTC), isApproximation);
}
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime) {
this(offsetDateTime, false);
}
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime,
final boolean isApproximation) {
this.offsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
this.isApproximation = isApproximation;
}
/**
* @return the wrapped date/time as a {@link Calendar}.
* @deprecated use {@link #offsetDateTime()} instead.
*/
@Deprecated
@Nonnull
public Calendar date() {
return GregorianCalendar.from(offsetDateTime.toZonedDateTime());
}
/**
* @return the wrapped date/time.
*/
@Nonnull
public OffsetDateTime offsetDateTime() {
return offsetDateTime;
}
/**
* @return if the date is considered is precise or just an approximation (e.g. service only
* returns an approximation like 2 weeks ago instead of a precise date).
*/
public boolean isApproximation() {
return isApproximation;
}
}

View File

@ -0,0 +1,129 @@
package org.schabi.newpipe.extractor.localization;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.utils.LocaleCompat;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class Localization implements Serializable {
public static final Localization DEFAULT = new Localization("en", "GB");
@Nonnull
private final String languageCode;
@Nullable
private final String countryCode;
/**
* @param localizationCodeList a list of localization code, formatted like {@link
* #getLocalizationCode()}
*/
public static List<Localization> listFrom(final String... localizationCodeList) {
final List<Localization> toReturn = new ArrayList<>();
for (final String localizationCode : localizationCodeList) {
toReturn.add(fromLocalizationCode(localizationCode));
}
return Collections.unmodifiableList(toReturn);
}
/**
* @param localizationCode a localization code, formatted like {@link #getLocalizationCode()}
*/
public static Localization fromLocalizationCode(final String localizationCode) {
return fromLocale(LocaleCompat.forLanguageTag(localizationCode));
}
public Localization(@Nonnull final String languageCode, @Nullable final String countryCode) {
this.languageCode = languageCode;
this.countryCode = countryCode;
}
public Localization(@Nonnull final String languageCode) {
this(languageCode, null);
}
@Nonnull
public String getLanguageCode() {
return languageCode;
}
@Nonnull
public String getCountryCode() {
return countryCode == null ? "" : countryCode;
}
public Locale asLocale() {
return new Locale(getLanguageCode(), getCountryCode());
}
public static Localization fromLocale(@Nonnull final Locale locale) {
return new Localization(locale.getLanguage(), locale.getCountry());
}
/**
* Return a formatted string in the form of: {@code language-Country}, or
* just {@code language} if country is {@code null}.
*/
public String getLocalizationCode() {
return languageCode + (countryCode == null ? "" : "-" + countryCode);
}
@Override
public String toString() {
return "Localization[" + getLocalizationCode() + "]";
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Localization)) {
return false;
}
final Localization that = (Localization) o;
return languageCode.equals(that.languageCode)
&& Objects.equals(countryCode, that.countryCode);
}
@Override
public int hashCode() {
int result = languageCode.hashCode();
result = 31 * result + Objects.hashCode(countryCode);
return result;
}
/**
* Converts a three letter language code (ISO 639-2/T) to a Locale
* because limits of Java Locale class.
*
* @param code a three letter language code
* @return the Locale corresponding
*/
public static Locale getLocaleFromThreeLetterCode(@Nonnull final String code)
throws ParsingException {
final String[] languages = Locale.getISOLanguages();
final Map<String, Locale> localeMap = new HashMap<>(languages.length);
for (final String language : languages) {
final Locale locale = new Locale(language);
localeMap.put(locale.getISO3Language(), locale);
}
if (localeMap.containsKey(code)) {
return localeMap.get(code);
} else {
throw new ParsingException(
"Could not get Locale from this three letter language code" + code);
}
}
}

View File

@ -0,0 +1,138 @@
package org.schabi.newpipe.extractor.localization;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
import org.schabi.newpipe.extractor.utils.Parser;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.regex.Pattern;
/**
* A helper class that is meant to be used by services that need to parse upload dates in the
* format '2 days ago' or similar.
*/
public class TimeAgoParser {
private final PatternsHolder patternsHolder;
private final OffsetDateTime now;
/**
* Creates a helper to parse upload dates in the format '2 days ago'.
* <p>
* Instantiate a new {@link TimeAgoParser} every time you extract a new batch of items.
* </p>
*
* @param patternsHolder An object that holds the "time ago" patterns, special cases, and the
* language word separator.
*/
public TimeAgoParser(final PatternsHolder patternsHolder) {
this.patternsHolder = patternsHolder;
now = OffsetDateTime.now(ZoneOffset.UTC);
}
/**
* Parses a textual date in the format '2 days ago' into a Calendar representation which is then
* wrapped in a {@link DateWrapper} object.
* <p>
* Beginning with days ago, the date is considered as an approximation.
*
* @param textualDate The original date as provided by the streaming service
* @return The parsed time (can be approximated)
* @throws ParsingException if the time unit could not be recognized
*/
public DateWrapper parse(final String textualDate) throws ParsingException {
for (final Map.Entry<ChronoUnit, Map<String, Integer>> caseUnitEntry
: patternsHolder.specialCases().entrySet()) {
final ChronoUnit chronoUnit = caseUnitEntry.getKey();
for (final Map.Entry<String, Integer> caseMapToAmountEntry
: caseUnitEntry.getValue().entrySet()) {
final String caseText = caseMapToAmountEntry.getKey();
final Integer caseAmount = caseMapToAmountEntry.getValue();
if (textualDateMatches(textualDate, caseText)) {
return getResultFor(caseAmount, chronoUnit);
}
}
}
return getResultFor(parseTimeAgoAmount(textualDate), parseChronoUnit(textualDate));
}
private int parseTimeAgoAmount(final String textualDate) {
try {
return Integer.parseInt(textualDate.replaceAll("\\D+", ""));
} catch (final NumberFormatException ignored) {
// If there is no valid number in the textual date,
// assume it is 1 (as in 'a second ago').
return 1;
}
}
private ChronoUnit parseChronoUnit(final String textualDate) throws ParsingException {
return patternsHolder.asMap().entrySet().stream()
.filter(e -> e.getValue().stream()
.anyMatch(agoPhrase -> textualDateMatches(textualDate, agoPhrase)))
.map(Map.Entry::getKey)
.findFirst()
.orElseThrow(() ->
new ParsingException("Unable to parse the date: " + textualDate));
}
private boolean textualDateMatches(final String textualDate, final String agoPhrase) {
if (textualDate.equals(agoPhrase)) {
return true;
}
if (patternsHolder.wordSeparator().isEmpty()) {
return textualDate.toLowerCase().contains(agoPhrase.toLowerCase());
}
final String escapedPhrase = Pattern.quote(agoPhrase.toLowerCase());
final String escapedSeparator = patternsHolder.wordSeparator().equals(" ")
// From JDK8 \h - Treat horizontal spaces as a normal one
// (non-breaking space, thin space, etc.)
? "[ \\t\\xA0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000]"
: Pattern.quote(patternsHolder.wordSeparator());
// (^|separator)pattern($|separator)
// Check if the pattern is surrounded by separators or start/end of the string.
final String pattern =
"(^|" + escapedSeparator + ")" + escapedPhrase + "($|" + escapedSeparator + ")";
return Parser.isMatch(pattern, textualDate.toLowerCase());
}
private DateWrapper getResultFor(final int timeAgoAmount, final ChronoUnit chronoUnit) {
OffsetDateTime offsetDateTime = now;
boolean isApproximation = false;
switch (chronoUnit) {
case SECONDS:
case MINUTES:
case HOURS:
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
break;
case DAYS:
case WEEKS:
case MONTHS:
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
isApproximation = true;
break;
case YEARS:
// minusDays is needed to prevent `PrettyTime` from showing '12 months ago'.
offsetDateTime = offsetDateTime.minusYears(timeAgoAmount).minusDays(1);
isApproximation = true;
break;
}
if (isApproximation) {
offsetDateTime = offsetDateTime.truncatedTo(ChronoUnit.HOURS);
}
return new DateWrapper(offsetDateTime, isApproximation);
}
}

View File

@ -0,0 +1,29 @@
package org.schabi.newpipe.extractor.localization;
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
import org.schabi.newpipe.extractor.timeago.PatternsManager;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class TimeAgoPatternsManager {
private TimeAgoPatternsManager() {
}
@Nullable
private static PatternsHolder getPatternsFor(@Nonnull final Localization localization) {
return PatternsManager.getPatterns(localization.getLanguageCode(),
localization.getCountryCode());
}
@Nullable
public static TimeAgoParser getTimeAgoParserFor(@Nonnull final Localization localization) {
final PatternsHolder holder = getPatternsFor(localization);
if (holder == null) {
return null;
}
return new TimeAgoParser(holder);
}
}

View File

@ -0,0 +1,54 @@
package org.schabi.newpipe.extractor.playlist;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import javax.annotation.Nonnull;
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
public PlaylistExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
public abstract String getUploaderUrl() throws ParsingException;
public abstract String getUploaderName() throws ParsingException;
public abstract String getUploaderAvatarUrl() throws ParsingException;
public abstract boolean isUploaderVerified() throws ParsingException;
public abstract long getStreamCount() throws ParsingException;
@Nonnull
public String getThumbnailUrl() throws ParsingException {
return "";
}
@Nonnull
public String getBannerUrl() throws ParsingException {
// Banner can't be handled by frontend right now.
// Whoever is willing to implement this should also implement it in the frontend.
return "";
}
@Nonnull
public String getSubChannelName() throws ParsingException {
return "";
}
@Nonnull
public String getSubChannelUrl() throws ParsingException {
return "";
}
@Nonnull
public String getSubChannelAvatarUrl() throws ParsingException {
return "";
}
public PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
return PlaylistInfo.PlaylistType.NORMAL;
}
}

View File

@ -0,0 +1,258 @@
package org.schabi.newpipe.extractor.playlist;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public final class PlaylistInfo extends ListInfo<StreamInfoItem> {
/**
* Mixes are handled as particular playlists in NewPipeExtractor. {@link PlaylistType#NORMAL} is
* for non-mixes, while other values are for the different types of mixes. The type of a mix
* depends on how its contents are autogenerated.
*/
public enum PlaylistType {
/**
* A normal playlist (not a mix)
*/
NORMAL,
/**
* A mix made only of streams related to a particular stream, for example YouTube mixes
*/
MIX_STREAM,
/**
* A mix made only of music streams related to a particular stream, for example YouTube
* music mixes
*/
MIX_MUSIC,
/**
* A mix made only of streams from (or related to) the same channel, for example YouTube
* channel mixes
*/
MIX_CHANNEL,
/**
* A mix made only of streams related to a particular (musical) genre, for example YouTube
* genre mixes
*/
MIX_GENRE,
}
@SuppressWarnings("RedundantThrows")
private PlaylistInfo(final int serviceId, final ListLinkHandler linkHandler, final String name)
throws ParsingException {
super(serviceId, linkHandler, name);
}
public static PlaylistInfo getInfo(final String url) throws IOException, ExtractionException {
return getInfo(NewPipe.getServiceByUrl(url), url);
}
public static PlaylistInfo getInfo(final StreamingService service, final String url)
throws IOException, ExtractionException {
final PlaylistExtractor extractor = service.getPlaylistExtractor(url);
extractor.fetchPage();
return getInfo(extractor);
}
public static InfoItemsPage<StreamInfoItem> getMoreItems(final StreamingService service,
final String url,
final Page page)
throws IOException, ExtractionException {
return service.getPlaylistExtractor(url).getPage(page);
}
/**
* Get PlaylistInfo from PlaylistExtractor
*
* @param extractor an extractor where fetchPage() was already got called on.
*/
public static PlaylistInfo getInfo(final PlaylistExtractor extractor)
throws ExtractionException {
final PlaylistInfo info = new PlaylistInfo(
extractor.getServiceId(),
extractor.getLinkHandler(),
extractor.getName());
// collect uploader extraction failures until we are sure this is not
// just a playlist without an uploader
final List<Throwable> uploaderParsingErrors = new ArrayList<>();
try {
info.setOriginalUrl(extractor.getOriginalUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setStreamCount(extractor.getStreamCount());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setThumbnailUrl(extractor.getThumbnailUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setUploaderUrl(extractor.getUploaderUrl());
} catch (final Exception e) {
info.setUploaderUrl("");
uploaderParsingErrors.add(e);
}
try {
info.setUploaderName(extractor.getUploaderName());
} catch (final Exception e) {
info.setUploaderName("");
uploaderParsingErrors.add(e);
}
try {
info.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
} catch (final Exception e) {
info.setUploaderAvatarUrl("");
uploaderParsingErrors.add(e);
}
try {
info.setSubChannelUrl(extractor.getSubChannelUrl());
} catch (final Exception e) {
uploaderParsingErrors.add(e);
}
try {
info.setSubChannelName(extractor.getSubChannelName());
} catch (final Exception e) {
uploaderParsingErrors.add(e);
}
try {
info.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl());
} catch (final Exception e) {
uploaderParsingErrors.add(e);
}
try {
info.setBannerUrl(extractor.getBannerUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setPlaylistType(extractor.getPlaylistType());
} catch (final Exception e) {
info.addError(e);
}
// do not fail if everything but the uploader infos could be collected (TODO better comment)
if (!uploaderParsingErrors.isEmpty()
&& (!info.getErrors().isEmpty() || uploaderParsingErrors.size() < 3)) {
info.addAllErrors(uploaderParsingErrors);
}
final InfoItemsPage<StreamInfoItem> itemsPage
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
info.setRelatedItems(itemsPage.getItems());
info.setNextPage(itemsPage.getNextPage());
return info;
}
private String thumbnailUrl;
private String bannerUrl;
private String uploaderUrl;
private String uploaderName;
private String uploaderAvatarUrl;
private String subChannelUrl;
private String subChannelName;
private String subChannelAvatarUrl;
private long streamCount = 0;
private PlaylistType playlistType;
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(final String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public String getBannerUrl() {
return bannerUrl;
}
public void setBannerUrl(final String bannerUrl) {
this.bannerUrl = bannerUrl;
}
public String getUploaderUrl() {
return uploaderUrl;
}
public void setUploaderUrl(final String uploaderUrl) {
this.uploaderUrl = uploaderUrl;
}
public String getUploaderName() {
return uploaderName;
}
public void setUploaderName(final String uploaderName) {
this.uploaderName = uploaderName;
}
public String getUploaderAvatarUrl() {
return uploaderAvatarUrl;
}
public void setUploaderAvatarUrl(final String uploaderAvatarUrl) {
this.uploaderAvatarUrl = uploaderAvatarUrl;
}
public String getSubChannelUrl() {
return subChannelUrl;
}
public void setSubChannelUrl(final String subChannelUrl) {
this.subChannelUrl = subChannelUrl;
}
public String getSubChannelName() {
return subChannelName;
}
public void setSubChannelName(final String subChannelName) {
this.subChannelName = subChannelName;
}
public String getSubChannelAvatarUrl() {
return subChannelAvatarUrl;
}
public void setSubChannelAvatarUrl(final String subChannelAvatarUrl) {
this.subChannelAvatarUrl = subChannelAvatarUrl;
}
public long getStreamCount() {
return streamCount;
}
public void setStreamCount(final long streamCount) {
this.streamCount = streamCount;
}
public PlaylistType getPlaylistType() {
return playlistType;
}
public void setPlaylistType(final PlaylistType playlistType) {
this.playlistType = playlistType;
}
}

View File

@ -0,0 +1,62 @@
package org.schabi.newpipe.extractor.playlist;
import org.schabi.newpipe.extractor.InfoItem;
import javax.annotation.Nullable;
public class PlaylistInfoItem extends InfoItem {
private String uploaderName;
private String uploaderUrl;
private boolean uploaderVerified;
/**
* How many streams this playlist have
*/
private long streamCount = 0;
private PlaylistInfo.PlaylistType playlistType;
public PlaylistInfoItem(final int serviceId, final String url, final String name) {
super(InfoType.PLAYLIST, serviceId, url, name);
}
public String getUploaderName() {
return uploaderName;
}
public void setUploaderName(final String uploaderName) {
this.uploaderName = uploaderName;
}
@Nullable
public String getUploaderUrl() {
return uploaderUrl;
}
public void setUploaderUrl(@Nullable final String uploaderUrl) {
this.uploaderUrl = uploaderUrl;
}
public boolean isUploaderVerified() {
return uploaderVerified;
}
public void setUploaderVerified(final boolean uploaderVerified) {
this.uploaderVerified = uploaderVerified;
}
public long getStreamCount() {
return streamCount;
}
public void setStreamCount(final long streamCount) {
this.streamCount = streamCount;
}
public PlaylistInfo.PlaylistType getPlaylistType() {
return playlistType;
}
public void setPlaylistType(final PlaylistInfo.PlaylistType playlistType) {
this.playlistType = playlistType;
}
}

View File

@ -0,0 +1,42 @@
package org.schabi.newpipe.extractor.playlist;
import org.schabi.newpipe.extractor.InfoItemExtractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import javax.annotation.Nonnull;
public interface PlaylistInfoItemExtractor extends InfoItemExtractor {
/**
* Get the uploader name
* @return the uploader name
*/
String getUploaderName() throws ParsingException;
/**
* Get the uploader url
* @return the uploader url
*/
String getUploaderUrl() throws ParsingException;
/**
* Get whether the uploader is verified
* @return whether the uploader is verified
*/
boolean isUploaderVerified() throws ParsingException;
/**
* Get the number of streams
* @return the number of streams
*/
long getStreamCount() throws ParsingException;
/**
* @return the type of this playlist, see {@link PlaylistInfo.PlaylistType} for a description
* of types. If not overridden always returns {@link PlaylistInfo.PlaylistType#NORMAL}.
*/
@Nonnull
default PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
return PlaylistInfo.PlaylistType.NORMAL;
}
}

View File

@ -0,0 +1,51 @@
package org.schabi.newpipe.extractor.playlist;
import org.schabi.newpipe.extractor.InfoItemsCollector;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
public class PlaylistInfoItemsCollector
extends InfoItemsCollector<PlaylistInfoItem, PlaylistInfoItemExtractor> {
public PlaylistInfoItemsCollector(final int serviceId) {
super(serviceId);
}
@Override
public PlaylistInfoItem extract(final PlaylistInfoItemExtractor extractor)
throws ParsingException {
final PlaylistInfoItem resultItem = new PlaylistInfoItem(
getServiceId(), extractor.getUrl(), extractor.getName());
try {
resultItem.setUploaderName(extractor.getUploaderName());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploaderUrl(extractor.getUploaderUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setUploaderVerified(extractor.isUploaderVerified());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setStreamCount(extractor.getStreamCount());
} catch (final Exception e) {
addError(e);
}
try {
resultItem.setPlaylistType(extractor.getPlaylistType());
} catch (final Exception e) {
addError(e);
}
return resultItem;
}
}

View File

@ -0,0 +1,72 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import javax.annotation.Nonnull;
import java.util.List;
public abstract class SearchExtractor extends ListExtractor<InfoItem> {
public static class NothingFoundException extends ExtractionException {
public NothingFoundException(final String message) {
super(message);
}
}
public SearchExtractor(final StreamingService service, final SearchQueryHandler linkHandler) {
super(service, linkHandler);
}
public String getSearchString() {
return getLinkHandler().getSearchString();
}
/**
* The search suggestion provided by the service.
* <p>
* This method also returns the corrected query if
* {@link SearchExtractor#isCorrectedSearch()} is true.
*
* @return a suggestion to another query, the corrected query, or an empty String.
*/
@Nonnull
public abstract String getSearchSuggestion() throws ParsingException;
@Nonnull
@Override
public SearchQueryHandler getLinkHandler() {
return (SearchQueryHandler) super.getLinkHandler();
}
@Nonnull
@Override
public String getName() {
return getLinkHandler().getSearchString();
}
/**
* Tell if the search was corrected by the service (if it's not exactly the search you typed).
* <p>
* Example: on YouTube, if you search for "pewdeipie",
* it will give you results for "pewdiepie", then isCorrectedSearch should return true.
*
* @return whether the results comes from a corrected query or not.
*/
public abstract boolean isCorrectedSearch() throws ParsingException;
/**
* Meta information about the search query.
* <p>
* Example: on YouTube, if you search for "Covid-19",
* there is a box with information from the WHO about Covid-19 and a link to the WHO's website.
* @return additional meta information about the search query
*/
@Nonnull
public abstract List<MetaInfo> getMetaInfo() throws ParsingException;
}

View File

@ -0,0 +1,113 @@
package org.schabi.newpipe.extractor.search;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
import java.io.IOException;
import java.util.List;
import javax.annotation.Nonnull;
public class SearchInfo extends ListInfo<InfoItem> {
private final String searchString;
private String searchSuggestion;
private boolean isCorrectedSearch;
private List<MetaInfo> metaInfo;
public SearchInfo(final int serviceId,
final SearchQueryHandler qIHandler,
final String searchString) {
super(serviceId, qIHandler, "Search");
this.searchString = searchString;
}
public static SearchInfo getInfo(final StreamingService service,
final SearchQueryHandler searchQuery)
throws ExtractionException, IOException {
final SearchExtractor extractor = service.getSearchExtractor(searchQuery);
extractor.fetchPage();
return getInfo(extractor);
}
public static SearchInfo getInfo(final SearchExtractor extractor)
throws ExtractionException, IOException {
final SearchInfo info = new SearchInfo(
extractor.getServiceId(),
extractor.getLinkHandler(),
extractor.getSearchString());
try {
info.setOriginalUrl(extractor.getOriginalUrl());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setSearchSuggestion(extractor.getSearchSuggestion());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setIsCorrectedSearch(extractor.isCorrectedSearch());
} catch (final Exception e) {
info.addError(e);
}
try {
info.setMetaInfo(extractor.getMetaInfo());
} catch (final Exception e) {
info.addError(e);
}
final ListExtractor.InfoItemsPage<InfoItem> page
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
info.setRelatedItems(page.getItems());
info.setNextPage(page.getNextPage());
return info;
}
public static ListExtractor.InfoItemsPage<InfoItem> getMoreItems(final StreamingService service,
final SearchQueryHandler query,
final Page page)
throws IOException, ExtractionException {
return service.getSearchExtractor(query).getPage(page);
}
// Getter
public String getSearchString() {
return this.searchString;
}
public String getSearchSuggestion() {
return this.searchSuggestion;
}
public boolean isCorrectedSearch() {
return this.isCorrectedSearch;
}
public void setIsCorrectedSearch(final boolean isCorrectedSearch) {
this.isCorrectedSearch = isCorrectedSearch;
}
public void setSearchSuggestion(final String searchSuggestion) {
this.searchSuggestion = searchSuggestion;
}
@Nonnull
public List<MetaInfo> getMetaInfo() {
return metaInfo;
}
public void setMetaInfo(@Nonnull final List<MetaInfo> metaInfo) {
this.metaInfo = metaInfo;
}
}

View File

@ -0,0 +1,156 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.FEATURED_API_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.KIOSK_FEATURED;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.KIOSK_RADIO;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.RADIO_API_URL;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.kiosk.KioskList;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampChannelExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampCommentsExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampPlaylistExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioStreamExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSearchExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSuggestionExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampChannelLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampCommentsLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampFeaturedLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampPlaylistLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampSearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampStreamLinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import java.util.Arrays;
public class BandcampService extends StreamingService {
public BandcampService(final int id) {
super(id, "Bandcamp", Arrays.asList(AUDIO, COMMENTS));
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public LinkHandlerFactory getStreamLHFactory() {
return new BandcampStreamLinkHandlerFactory();
}
@Override
public ListLinkHandlerFactory getChannelLHFactory() {
return new BandcampChannelLinkHandlerFactory();
}
@Override
public ListLinkHandlerFactory getPlaylistLHFactory() {
return new BandcampPlaylistLinkHandlerFactory();
}
@Override
public SearchQueryHandlerFactory getSearchQHFactory() {
return new BandcampSearchQueryHandlerFactory();
}
@Override
public ListLinkHandlerFactory getCommentsLHFactory() {
return new BandcampCommentsLinkHandlerFactory();
}
@Override
public SearchExtractor getSearchExtractor(final SearchQueryHandler queryHandler) {
return new BandcampSearchExtractor(this, queryHandler);
}
@Override
public SuggestionExtractor getSuggestionExtractor() {
return new BandcampSuggestionExtractor(this);
}
@Override
public SubscriptionExtractor getSubscriptionExtractor() {
return null;
}
@Override
public KioskList getKioskList() throws ExtractionException {
final KioskList kioskList = new KioskList(this);
try {
kioskList.addKioskEntry(
(streamingService, url, kioskId) -> new BandcampFeaturedExtractor(
BandcampService.this,
new BandcampFeaturedLinkHandlerFactory().fromUrl(FEATURED_API_URL),
kioskId
),
new BandcampFeaturedLinkHandlerFactory(),
KIOSK_FEATURED
);
kioskList.addKioskEntry(
(streamingService, url, kioskId) -> new BandcampRadioExtractor(
BandcampService.this,
new BandcampFeaturedLinkHandlerFactory().fromUrl(RADIO_API_URL),
kioskId
),
new BandcampFeaturedLinkHandlerFactory(),
KIOSK_RADIO
);
kioskList.setDefaultKiosk(KIOSK_FEATURED);
} catch (final Exception e) {
throw new ExtractionException(e);
}
return kioskList;
}
@Override
public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
return new BandcampChannelExtractor(this, linkHandler);
}
@Override
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
return new BandcampPlaylistExtractor(this, linkHandler);
}
@Override
public StreamExtractor getStreamExtractor(final LinkHandler linkHandler) {
if (BandcampExtractorHelper.isRadioUrl(linkHandler.getUrl())) {
return new BandcampRadioStreamExtractor(this, linkHandler);
}
return new BandcampStreamExtractor(this, linkHandler);
}
@Override
public CommentsExtractor getCommentsExtractor(final ListLinkHandler linkHandler) {
return new BandcampCommentsExtractor(this, linkHandler);
}
}

View File

@ -0,0 +1,146 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import org.jsoup.Jsoup;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampDiscographStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import java.io.IOException;
import java.util.Objects;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
public class BandcampChannelExtractor extends ChannelExtractor {
private JsonObject channelInfo;
public BandcampChannelExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
@Override
public String getAvatarUrl() {
if (channelInfo.getLong("bio_image_id") == 0) {
return "";
}
return BandcampExtractorHelper.getImageUrl(channelInfo.getLong("bio_image_id"), false);
}
@Override
public String getBannerUrl() throws ParsingException {
/*
* Mobile API does not return the header or not the correct header.
* Therefore, we need to query the website
*/
try {
final String html = getDownloader()
.get(replaceHttpWithHttps(channelInfo.getString("bandcamp_url")))
.responseBody();
return Stream.of(Jsoup.parse(html).getElementById("customHeader"))
.filter(Objects::nonNull)
.flatMap(element -> element.getElementsByTag("img").stream())
.map(element -> element.attr("src"))
.findFirst()
.orElse(""); // no banner available
} catch (final IOException | ReCaptchaException e) {
throw new ParsingException("Could not download artist web site", e);
}
}
/**
* Bandcamp discontinued their RSS feeds because it hadn't been used enough.
*/
@Override
public String getFeedUrl() {
return null;
}
@Override
public long getSubscriberCount() {
return -1;
}
@Override
public String getDescription() {
return channelInfo.getString("bio");
}
@Override
public String getParentChannelName() {
return null;
}
@Override
public String getParentChannelUrl() {
return null;
}
@Override
public String getParentChannelAvatarUrl() {
return null;
}
@Override
public boolean isVerified() throws ParsingException {
return false;
}
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ParsingException {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
final JsonArray discography = channelInfo.getArray("discography");
for (int i = 0; i < discography.size(); i++) {
// A discograph is as an item appears in a discography
final JsonObject discograph = discography.getObject(i);
if (!discograph.getString("item_type").equals("track")) {
continue;
}
collector.commit(new BandcampDiscographStreamInfoItemExtractor(discograph, getUrl()));
}
return new InfoItemsPage<>(collector, null);
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
return null;
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
channelInfo = BandcampExtractorHelper.getArtistDetails(getId());
}
@Nonnull
@Override
public String getName() {
return channelInfo.getString("name");
}
}

View File

@ -0,0 +1,53 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
public class BandcampChannelInfoItemExtractor implements ChannelInfoItemExtractor {
private final Element resultInfo;
private final Element searchResult;
public BandcampChannelInfoItemExtractor(final Element searchResult) {
this.searchResult = searchResult;
resultInfo = searchResult.getElementsByClass("result-info").first();
}
@Override
public String getName() throws ParsingException {
return resultInfo.getElementsByClass("heading").text();
}
@Override
public String getUrl() throws ParsingException {
return resultInfo.getElementsByClass("itemurl").text();
}
@Override
public String getThumbnailUrl() throws ParsingException {
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
}
@Override
public String getDescription() {
return resultInfo.getElementsByClass("subhead").text();
}
@Override
public long getSubscriberCount() {
return -1;
}
@Override
public long getStreamCount() {
return -1;
}
@Override
public boolean isVerified() throws ParsingException {
return false;
}
}

View File

@ -0,0 +1,56 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import javax.annotation.Nonnull;
import java.io.IOException;
public class BandcampCommentsExtractor extends CommentsExtractor {
private Document document;
public BandcampCommentsExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
document = Jsoup.parse(downloader.get(getLinkHandler().getUrl()).responseBody());
}
@Nonnull
@Override
public InfoItemsPage<CommentsInfoItem> getInitialPage()
throws IOException, ExtractionException {
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
final Elements writings = document.getElementsByClass("writing");
for (final Element writing : writings) {
collector.commit(new BandcampCommentsInfoItemExtractor(writing, getUrl()));
}
return new InfoItemsPage<>(collector, null);
}
@Override
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
throws IOException, ExtractionException {
return null;
}
}

View File

@ -0,0 +1,59 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream.Description;
import java.util.Objects;
public class BandcampCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
private final Element writing;
private final String url;
public BandcampCommentsInfoItemExtractor(final Element writing, final String url) {
this.writing = writing;
this.url = url;
}
@Override
public String getName() throws ParsingException {
return getCommentText().getContent();
}
@Override
public String getUrl() {
return url;
}
@Override
public String getThumbnailUrl() throws ParsingException {
return writing.getElementsByClass("thumb").attr("src");
}
@Override
public Description getCommentText() throws ParsingException {
final var text = writing.getElementsByClass("text").stream()
.filter(Objects::nonNull)
.map(Element::ownText)
.findFirst()
.orElseThrow(() -> new ParsingException("Could not get comment text"));
return new Description(text, Description.PLAIN_TEXT);
}
@Override
public String getUploaderName() throws ParsingException {
return writing.getElementsByClass("name").stream()
.filter(Objects::nonNull)
.map(Element::text)
.findFirst()
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
}
@Override
public String getUploaderAvatarUrl() {
return writing.getElementsByClass("thumb").attr("src");
}
}

View File

@ -0,0 +1,148 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonWriter;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.utils.Utils;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Locale;
public final class BandcampExtractorHelper {
public static final String BASE_URL = "https://bandcamp.com";
public static final String BASE_API_URL = BASE_URL + "/api";
private BandcampExtractorHelper() {
}
/**
* Translate all these parameters together to the URL of the corresponding album or track
* using the mobile API
*/
public static String getStreamUrlFromIds(final long bandId,
final long itemId,
final String itemType) throws ParsingException {
try {
final String jsonString = NewPipe.getDownloader().get(
BASE_API_URL + "/mobile/22/tralbum_details?band_id=" + bandId
+ "&tralbum_id=" + itemId + "&tralbum_type=" + itemType.charAt(0))
.responseBody();
return Utils.replaceHttpWithHttps(JsonParser.object().from(jsonString)
.getString("bandcamp_url"));
} catch (final JsonParserException | ReCaptchaException | IOException e) {
throw new ParsingException("Ids could not be translated to URL", e);
}
}
/**
* Fetch artist details from mobile endpoint.
* <a href="https://notabug.org/fynngodau/bandcampDirect/wiki/
* rewindBandcamp+%E2%80%93+Fetching+artist+details">
* More technical info.</a>
*/
public static JsonObject getArtistDetails(final String id) throws ParsingException {
try {
return JsonParser.object().from(NewPipe.getDownloader().postWithContentTypeJson(
BASE_API_URL + "/mobile/22/band_details",
Collections.emptyMap(),
JsonWriter.string()
.object()
.value("band_id", id)
.end()
.done()
.getBytes(StandardCharsets.UTF_8)).responseBody());
} catch (final IOException | ReCaptchaException | JsonParserException e) {
throw new ParsingException("Could not download band details", e);
}
}
/**
* Generate image url from image ID.
* <p>
* The appendix "_10" was chosen because it provides images sized 1200x1200. Other integer
* values are possible as well (e.g. 0 is a very large resolution, possibly the original).
*
* @param id The image ID
* @param album True if this is the cover of an album or track
* @return URL of image with this ID sized 1200x1200
*/
public static String getImageUrl(final long id, final boolean album) {
return "https://f4.bcbits.com/img/" + (album ? 'a' : "") + id + "_10.jpg";
}
/**
* @return <code>true</code> if the given URL looks like it comes from a bandcamp custom domain
* or if it comes from <code>bandcamp.com</code> itself
*/
public static boolean isSupportedDomain(final String url) throws ParsingException {
// Accept all bandcamp.com URLs
if (url.toLowerCase().matches("https?://.+\\.bandcamp\\.com(/.*)?")) {
return true;
}
try {
// Test other URLs for whether they contain a footer that links to bandcamp
return Jsoup.parse(NewPipe.getDownloader().get(url).responseBody())
.getElementById("pgFt")
.getElementById("pgFt-inner")
.getElementById("footer-logo-wrapper")
.getElementById("footer-logo")
.getElementsByClass("hiddenAccess")
.text().equals("Bandcamp");
} catch (final NullPointerException e) {
return false;
} catch (final IOException | ReCaptchaException e) {
throw new ParsingException("Could not determine whether URL is custom domain "
+ "(not available? network error?)");
}
}
/**
* Whether the URL points to a radio kiosk.
* @param url the URL to check
* @return true if the URL matches {@code https://bandcamp.com/?show=SHOW_ID}
*/
public static boolean isRadioUrl(final String url) {
return url.toLowerCase().matches("https?://bandcamp\\.com/\\?show=\\d+");
}
public static DateWrapper parseDate(final String textDate) throws ParsingException {
try {
final ZonedDateTime zonedDateTime = ZonedDateTime.parse(textDate,
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH));
return new DateWrapper(zonedDateTime.toOffsetDateTime(), false);
} catch (final DateTimeException e) {
throw new ParsingException("Could not parse date '" + textDate + "'", e);
}
}
@Nullable
public static String getThumbnailUrlFromSearchResult(final Element searchResult) {
return searchResult.getElementsByClass("art").stream()
.flatMap(element -> element.getElementsByTag("img").stream())
.map(element -> element.attr("src"))
.filter(string -> !string.isEmpty())
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,118 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
public class BandcampFeaturedExtractor extends KioskExtractor<PlaylistInfoItem> {
public static final String KIOSK_FEATURED = "Featured";
public static final String FEATURED_API_URL = BASE_API_URL + "/mobile/24/bootstrap_data";
public static final String MORE_FEATURED_API_URL
= BASE_API_URL + "/mobile/24/feed_older_logged_out";
private JsonObject json;
public BandcampFeaturedExtractor(final StreamingService streamingService,
final ListLinkHandler listLinkHandler,
final String kioskId) {
super(streamingService, listLinkHandler, kioskId);
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
try {
json = JsonParser.object().from(getDownloader().postWithContentTypeJson(
FEATURED_API_URL,
Collections.emptyMap(),
"{\"platform\":\"\",\"version\":0}".getBytes(StandardCharsets.UTF_8))
.responseBody());
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse Bandcamp featured API response", e);
}
}
@Nonnull
@Override
public String getName() throws ParsingException {
return KIOSK_FEATURED;
}
@Nonnull
@Override
public InfoItemsPage<PlaylistInfoItem> getInitialPage()
throws IOException, ExtractionException {
final JsonArray featuredStories = json.getObject("feed_content")
.getObject("stories")
.getArray("featured");
return extractItems(featuredStories);
}
private InfoItemsPage<PlaylistInfoItem> extractItems(final JsonArray featuredStories) {
final PlaylistInfoItemsCollector c = new PlaylistInfoItemsCollector(getServiceId());
for (int i = 0; i < featuredStories.size(); i++) {
final JsonObject featuredStory = featuredStories.getObject(i);
if (featuredStory.isNull("album_title")) {
// Is not an album, ignore
continue;
}
c.commit(new BandcampPlaylistInfoItemFeaturedExtractor(featuredStory));
}
final JsonObject lastFeaturedStory = featuredStories.getObject(featuredStories.size() - 1);
return new InfoItemsPage<>(c, getNextPageFrom(lastFeaturedStory));
}
/**
* Next Page can be generated from metadata of last featured story
*/
private Page getNextPageFrom(final JsonObject lastFeaturedStory) {
final long lastStoryDate = lastFeaturedStory.getLong("story_date");
final long lastStoryId = lastFeaturedStory.getLong("ntid");
final String lastStoryType = lastFeaturedStory.getString("story_type");
return new Page(
MORE_FEATURED_API_URL + "?story_groups=featured"
+ ':' + lastStoryDate + ':' + lastStoryType + ':' + lastStoryId
);
}
@Override
public InfoItemsPage<PlaylistInfoItem> getPage(final Page page)
throws IOException, ExtractionException {
final JsonObject response;
try {
response = JsonParser.object().from(
getDownloader().get(page.getUrl()).responseBody()
);
} catch (final JsonParserException e) {
throw new ParsingException("Could not parse Bandcamp featured API response", e);
}
return extractItems(response.getObject("stories").getArray("featured"));
}
}

View File

@ -0,0 +1,144 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor.getAlbumInfoJson;
import static org.schabi.newpipe.extractor.utils.JsonUtils.getJsonData;
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParserException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampPlaylistStreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import java.io.IOException;
import javax.annotation.Nonnull;
public class BandcampPlaylistExtractor extends PlaylistExtractor {
/**
* An arbitrarily chosen number above which cover arts won't be fetched individually for each
* track; instead, it will be assumed that every track has the same cover art as the album,
* which is not always the case.
*/
private static final int MAXIMUM_INDIVIDUAL_COVER_ARTS = 10;
private Document document;
private JsonObject albumJson;
private JsonArray trackInfo;
private String name;
public BandcampPlaylistExtractor(final StreamingService service,
final ListLinkHandler linkHandler) {
super(service, linkHandler);
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
document = Jsoup.parse(html);
albumJson = getAlbumInfoJson(html);
trackInfo = albumJson.getArray("trackinfo");
try {
name = getJsonData(html, "data-embed").getString("album_title");
} catch (final JsonParserException e) {
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
} catch (final ArrayIndexOutOfBoundsException e) {
throw new ParsingException("JSON does not exist", e);
}
if (trackInfo.isEmpty()) {
// Albums without trackInfo need to be purchased before they can be played
throw new ContentNotAvailableException("Album needs to be purchased");
}
}
@Nonnull
@Override
public String getThumbnailUrl() throws ParsingException {
if (albumJson.isNull("art_id")) {
return "";
} else {
return getImageUrl(albumJson.getLong("art_id"), true);
}
}
@Override
public String getUploaderUrl() throws ParsingException {
final String[] parts = getUrl().split("/");
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
return HTTPS + parts[2] + "/";
}
@Override
public String getUploaderName() {
return albumJson.getString("artist");
}
@Override
public String getUploaderAvatarUrl() {
return document.getElementsByClass("band-photo").stream()
.map(element -> element.attr("src"))
.findFirst()
.orElse("");
}
@Override
public boolean isUploaderVerified() throws ParsingException {
return false;
}
@Override
public long getStreamCount() {
return trackInfo.size();
}
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
for (int i = 0; i < trackInfo.size(); i++) {
final JsonObject track = trackInfo.getObject(i);
if (trackInfo.size() < MAXIMUM_INDIVIDUAL_COVER_ARTS) {
// Load cover art of every track individually
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
track, getUploaderUrl(), getService()));
} else {
// Pretend every track has the same cover art as the album
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
track, getUploaderUrl(), getThumbnailUrl()));
}
}
return new InfoItemsPage<>(collector, null);
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
return null;
}
@Nonnull
@Override
public String getName() throws ParsingException {
return name;
}
}

View File

@ -0,0 +1,53 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import javax.annotation.Nonnull;
public class BandcampPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
private final Element searchResult;
private final Element resultInfo;
public BandcampPlaylistInfoItemExtractor(@Nonnull final Element searchResult) {
this.searchResult = searchResult;
resultInfo = searchResult.getElementsByClass("result-info").first();
}
@Override
public String getUploaderName() {
return resultInfo.getElementsByClass("subhead").text()
.split(" by")[0];
}
@Override
public String getUploaderUrl() {
return null;
}
@Override
public boolean isUploaderVerified() {
return false;
}
@Override
public long getStreamCount() {
final String length = resultInfo.getElementsByClass("length").text();
return Integer.parseInt(length.split(" track")[0]);
}
@Override
public String getName() {
return resultInfo.getElementsByClass("heading").text();
}
@Override
public String getUrl() {
return resultInfo.getElementsByClass("itemurl").text();
}
@Override
public String getThumbnailUrl() {
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
}
}

View File

@ -0,0 +1,51 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
public class BandcampPlaylistInfoItemFeaturedExtractor implements PlaylistInfoItemExtractor {
private final JsonObject featuredStory;
public BandcampPlaylistInfoItemFeaturedExtractor(final JsonObject featuredStory) {
this.featuredStory = featuredStory;
}
@Override
public String getUploaderName() {
return featuredStory.getString("band_name");
}
@Override
public String getUploaderUrl() {
return null;
}
@Override
public boolean isUploaderVerified() {
return false;
}
@Override
public long getStreamCount() {
return featuredStory.getInt("num_streamable_tracks");
}
@Override
public String getName() {
return featuredStory.getString("album_title");
}
@Override
public String getUrl() {
return featuredStory.getString("item_url").replaceAll("http://", "https://");
}
@Override
public String getThumbnailUrl() {
return featuredStory.has("art_id") ? getImageUrl(featuredStory.getLong("art_id"), true)
: getImageUrl(featuredStory.getLong("item_art_id"), true);
}
}

View File

@ -0,0 +1,73 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
import javax.annotation.Nonnull;
import java.io.IOException;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
public class BandcampRadioExtractor extends KioskExtractor<StreamInfoItem> {
public static final String KIOSK_RADIO = "Radio";
public static final String RADIO_API_URL = BASE_API_URL + "/bcweekly/1/list";
private JsonObject json = null;
public BandcampRadioExtractor(final StreamingService streamingService,
final ListLinkHandler linkHandler,
final String kioskId) {
super(streamingService, linkHandler, kioskId);
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
try {
json = JsonParser.object().from(
getDownloader().get(RADIO_API_URL).responseBody());
} catch (final JsonParserException e) {
throw new ExtractionException("Could not parse Bandcamp Radio API response", e);
}
}
@Nonnull
@Override
public String getName() throws ParsingException {
return KIOSK_RADIO;
}
@Nonnull
@Override
public InfoItemsPage<StreamInfoItem> getInitialPage() {
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
final JsonArray radioShows = json.getArray("results");
for (int i = 0; i < radioShows.size(); i++) {
final JsonObject radioShow = radioShows.getObject(i);
collector.commit(new BandcampRadioInfoItemExtractor(radioShow));
}
return new InfoItemsPage<>(collector, null);
}
@Override
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
return null;
}
}

View File

@ -0,0 +1,96 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
private final JsonObject show;
public BandcampRadioInfoItemExtractor(final JsonObject radioShow) {
show = radioShow;
}
@Override
public long getDuration() {
/* Duration is only present in the more detailed information that has to be queried
separately. Therefore, over 300 queries would be needed every time the kiosk is opened if we
were to display the real value. */
//return query(show.getInt("id")).getLong("audio_duration");
return 0;
}
@Nullable
@Override
public String getTextualUploadDate() {
return show.getString("date");
}
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
return BandcampExtractorHelper.parseDate(getTextualUploadDate());
}
@Override
public String getName() throws ParsingException {
return show.getString("subtitle");
}
@Override
public String getUrl() {
return BASE_URL + "/?show=" + show.getInt("id");
}
@Override
public String getThumbnailUrl() {
return getImageUrl(show.getLong("image_id"), false);
}
@Override
public StreamType getStreamType() {
return StreamType.AUDIO_STREAM;
}
@Override
public long getViewCount() {
return -1;
}
@Override
public String getUploaderName() {
// JSON does not contain uploader name
return "";
}
@Override
public String getUploaderUrl() {
return "";
}
@Nullable
@Override
public String getUploaderAvatarUrl() {
return null;
}
@Override
public boolean isUploaderVerified() throws ParsingException {
return false;
}
@Override
public boolean isAd() {
return false;
}
}

View File

@ -0,0 +1,189 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamSegment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
private static final String OPUS_LO = "opus-lo";
private static final String MP3_128 = "mp3-128";
private JsonObject showInfo;
public BandcampRadioStreamExtractor(final StreamingService service,
final LinkHandler linkHandler) {
super(service, linkHandler);
}
static JsonObject query(final int id) throws ParsingException {
try {
return JsonParser.object().from(NewPipe.getDownloader()
.get(BASE_API_URL + "/bcweekly/1/get?id=" + id).responseBody());
} catch (final IOException | ReCaptchaException | JsonParserException e) {
throw new ParsingException("could not get show data", e);
}
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
showInfo = query(Integer.parseInt(getId()));
}
@Nonnull
@Override
public String getName() throws ParsingException {
/* Select "subtitle" and not "audio_title", as the latter would cause a lot of
* items to show the same title, e.g. "Bandcamp Weekly".
*/
return showInfo.getString("subtitle");
}
@Nonnull
@Override
public String getUploaderUrl() throws ContentNotSupportedException {
throw new ContentNotSupportedException("Fan pages are not supported");
}
@Nonnull
@Override
public String getUrl() throws ParsingException {
return getLinkHandler().getUrl();
}
@Nonnull
@Override
public String getUploaderName() throws ParsingException {
return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream()
.map(Element::text)
.findFirst()
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
}
@Nullable
@Override
public String getTextualUploadDate() {
return showInfo.getString("published_date");
}
@Nonnull
@Override
public String getThumbnailUrl() throws ParsingException {
return getImageUrl(showInfo.getLong("show_image_id"), false);
}
@Nonnull
@Override
public String getUploaderAvatarUrl() {
return BASE_URL + "/img/buttons/bandcamp-button-circle-whitecolor-512.png";
}
@Nonnull
@Override
public Description getDescription() {
return new Description(showInfo.getString("desc"), Description.PLAIN_TEXT);
}
@Override
public long getLength() {
return showInfo.getLong("audio_duration");
}
@Override
public List<AudioStream> getAudioStreams() {
final List<AudioStream> audioStreams = new ArrayList<>();
final JsonObject streams = showInfo.getObject("audio_stream");
if (streams.has(MP3_128)) {
audioStreams.add(new AudioStream.Builder()
.setId(MP3_128)
.setContent(streams.getString(MP3_128), true)
.setMediaFormat(MediaFormat.MP3)
.setAverageBitrate(128)
.build());
}
if (streams.has(OPUS_LO)) {
audioStreams.add(new AudioStream.Builder()
.setId(OPUS_LO)
.setContent(streams.getString(OPUS_LO), true)
.setMediaFormat(MediaFormat.OPUS)
.setAverageBitrate(100).build());
}
return audioStreams;
}
@Nonnull
@Override
public List<StreamSegment> getStreamSegments() throws ParsingException {
final JsonArray tracks = showInfo.getArray("tracks");
final List<StreamSegment> segments = new ArrayList<>(tracks.size());
for (final Object t : tracks) {
final JsonObject track = (JsonObject) t;
final StreamSegment segment = new StreamSegment(
track.getString("title"), track.getInt("timecode"));
// "track art" is the track's album cover
segment.setPreviewUrl(getImageUrl(track.getLong("track_art_id"), true));
segment.setChannelName(track.getString("artist"));
segments.add(segment);
}
return segments;
}
@Nonnull
@Override
public String getLicence() {
// Contrary to other Bandcamp streams, radio streams don't have a license
return "";
}
@Nonnull
@Override
public String getCategory() {
// Contrary to other Bandcamp streams, radio streams don't have categories
return "";
}
@Nonnull
@Override
public List<String> getTags() {
// Contrary to other Bandcamp streams, radio streams don't have tags
return Collections.emptyList();
}
@Override
public PlaylistInfoItemsCollector getRelatedItems() {
// Contrary to other Bandcamp streams, radio streams don't have related items
return null;
}
}

View File

@ -0,0 +1,55 @@
// Created by Fynn Godau 2021, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
import javax.annotation.Nonnull;
/**
* Extracts recommended albums from tracks' website
*/
public class BandcampRelatedPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
private final Element relatedAlbum;
public BandcampRelatedPlaylistInfoItemExtractor(@Nonnull final Element relatedAlbum) {
this.relatedAlbum = relatedAlbum;
}
@Override
public String getName() throws ParsingException {
return relatedAlbum.getElementsByClass("release-title").text();
}
@Override
public String getUrl() throws ParsingException {
return relatedAlbum.getElementsByClass("title-and-artist").attr("abs:href");
}
@Override
public String getThumbnailUrl() throws ParsingException {
return relatedAlbum.getElementsByClass("album-art").attr("src");
}
@Override
public String getUploaderName() throws ParsingException {
return relatedAlbum.getElementsByClass("by-artist").text().replace("by ", "");
}
@Override
public String getUploaderUrl() throws ParsingException {
return null;
}
@Override
public boolean isUploaderVerified() throws ParsingException {
return false;
}
@Override
public long getStreamCount() throws ParsingException {
return -1;
}
}

View File

@ -0,0 +1,122 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.MetaInfo;
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampSearchStreamInfoItemExtractor;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class BandcampSearchExtractor extends SearchExtractor {
public BandcampSearchExtractor(final StreamingService service,
final SearchQueryHandler linkHandler) {
super(service, linkHandler);
}
@Nonnull
@Override
public String getSearchSuggestion() {
return "";
}
@Override
public boolean isCorrectedSearch() {
return false;
}
@Nonnull
@Override
public List<MetaInfo> getMetaInfo() throws ParsingException {
return Collections.emptyList();
}
public InfoItemsPage<InfoItem> getPage(final Page page)
throws IOException, ExtractionException {
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
final Document d = Jsoup.parse(getDownloader().get(page.getUrl()).responseBody());
for (final Element searchResult : d.getElementsByClass("searchresult")) {
final String type = searchResult.getElementsByClass("result-info").stream()
.flatMap(element -> element.getElementsByClass("itemtype").stream())
.map(Element::text)
.findFirst()
.orElse("");
switch (type) {
case "ARTIST":
collector.commit(new BandcampChannelInfoItemExtractor(searchResult));
break;
case "ALBUM":
collector.commit(new BandcampPlaylistInfoItemExtractor(searchResult));
break;
case "TRACK":
collector.commit(new BandcampSearchStreamInfoItemExtractor(searchResult, null));
break;
default:
// don't display fan results ("FAN") or other things
break;
}
}
// Count pages
final Elements pageLists = d.getElementsByClass("pagelist");
if (pageLists.isEmpty()) {
return new InfoItemsPage<>(collector, null);
}
final Elements pages = pageLists.stream()
.map(element -> element.getElementsByTag("li"))
.findFirst()
.orElseGet(Elements::new);
// Find current page
int currentPage = -1;
for (int i = 0; i < pages.size(); i++) {
final Element pageElement = pages.get(i);
if (!pageElement.getElementsByTag("span").isEmpty()) {
currentPage = i + 1;
break;
}
}
// Search results appear to be capped at six pages
assert pages.size() < 10;
String nextUrl = null;
if (currentPage < pages.size()) {
nextUrl = page.getUrl().substring(0, page.getUrl().length() - 1) + (currentPage + 1);
}
return new InfoItemsPage<>(collector, new Page(nextUrl));
}
@Nonnull
@Override
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
return getPage(new Page(getUrl()));
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
}
}

View File

@ -0,0 +1,237 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParserException;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class BandcampStreamExtractor extends StreamExtractor {
private JsonObject albumJson;
private JsonObject current;
private Document document;
public BandcampStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
super(service, linkHandler);
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
document = Jsoup.parse(html);
albumJson = getAlbumInfoJson(html);
current = albumJson.getObject("current");
if (albumJson.getArray("trackinfo").size() > 1) {
// In this case, we are actually viewing an album page!
throw new ExtractionException("Page is actually an album, not a track");
}
}
/**
* Get the JSON that contains album's metadata from page
*
* @param html Website
* @return Album metadata JSON
* @throws ParsingException In case of a faulty website
*/
public static JsonObject getAlbumInfoJson(final String html) throws ParsingException {
try {
return JsonUtils.getJsonData(html, "data-tralbum");
} catch (final JsonParserException e) {
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
} catch (final ArrayIndexOutOfBoundsException e) {
throw new ParsingException("JSON does not exist", e);
}
}
@Nonnull
@Override
public String getName() throws ParsingException {
return current.getString("title");
}
@Nonnull
@Override
public String getUploaderUrl() throws ParsingException {
final String[] parts = getUrl().split("/");
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
return HTTPS + parts[2] + "/";
}
@Nonnull
@Override
public String getUrl() throws ParsingException {
return albumJson.getString("url").replace("http://", "https://");
}
@Nonnull
@Override
public String getUploaderName() throws ParsingException {
return albumJson.getString("artist");
}
@Nullable
@Override
public String getTextualUploadDate() {
return current.getString("publish_date");
}
@Nullable
@Override
public DateWrapper getUploadDate() throws ParsingException {
return BandcampExtractorHelper.parseDate(getTextualUploadDate());
}
@Nonnull
@Override
public String getThumbnailUrl() throws ParsingException {
if (albumJson.isNull("art_id")) {
return "";
}
return getImageUrl(albumJson.getLong("art_id"), true);
}
@Nonnull
@Override
public String getUploaderAvatarUrl() {
return document.getElementsByClass("band-photo").stream()
.map(element -> element.attr("src"))
.findFirst()
.orElse("");
}
@Nonnull
@Override
public Description getDescription() {
final String s = Utils.nonEmptyAndNullJoin("\n\n", current.getString("about"),
current.getString("lyrics"), current.getString("credits"));
return new Description(s, Description.PLAIN_TEXT);
}
@Override
public List<AudioStream> getAudioStreams() {
return Collections.singletonList(new AudioStream.Builder()
.setId("mp3-128")
.setContent(albumJson.getArray("trackinfo")
.getObject(0)
.getObject("file")
.getString("mp3-128"), true)
.setMediaFormat(MediaFormat.MP3)
.setAverageBitrate(128)
.build());
}
@Override
public long getLength() throws ParsingException {
return (long) albumJson.getArray("trackinfo").getObject(0)
.getDouble("duration");
}
@Override
public List<VideoStream> getVideoStreams() {
return Collections.emptyList();
}
@Override
public List<VideoStream> getVideoOnlyStreams() {
return Collections.emptyList();
}
@Override
public StreamType getStreamType() {
return StreamType.AUDIO_STREAM;
}
@Override
public PlaylistInfoItemsCollector getRelatedItems() {
final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId());
document.getElementsByClass("recommended-album")
.stream()
.map(BandcampRelatedPlaylistInfoItemExtractor::new)
.forEach(collector::commit);
return collector;
}
@Nonnull
@Override
public String getCategory() {
// Get first tag from html, which is the artist's Genre
return document.getElementsByClass("tralbum-tags").stream()
.flatMap(element -> element.getElementsByClass("tag").stream())
.map(Element::text)
.findFirst()
.orElse("");
}
@Nonnull
@Override
public String getLicence() {
/*
Tests resulted in this mapping of ints to licence:
https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's
account)
*/
switch (current.getInt("license_type")) {
case 1:
return "All rights reserved ©";
case 2:
return "CC BY-NC-ND 3.0";
case 3:
return "CC BY-NC-SA 3.0";
case 4:
return "CC BY-NC 3.0";
case 5:
return "CC BY-ND 3.0";
case 6:
return "CC BY 3.0";
case 8:
return "CC BY-SA 3.0";
default:
return "Unknown";
}
}
@Nonnull
@Override
public List<String> getTags() {
return document.getElementsByAttributeValue("itemprop", "keywords")
.stream()
.map(Element::text)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,48 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class BandcampSuggestionExtractor extends SuggestionExtractor {
private static final String AUTOCOMPLETE_URL = BASE_API_URL + "/fuzzysearch/1/autocomplete?q=";
public BandcampSuggestionExtractor(final StreamingService service) {
super(service);
}
@Override
public List<String> suggestionList(final String query) throws IOException, ExtractionException {
final Downloader downloader = NewPipe.getDownloader();
try {
final JsonObject fuzzyResults = JsonParser.object().from(downloader
.get(AUTOCOMPLETE_URL + Utils.encodeUrlUtf8(query)).responseBody());
return fuzzyResults.getObject("auto").getArray("results").stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)
.map(jsonObject -> jsonObject.getString("name"))
.distinct()
.collect(Collectors.toList());
} catch (final JsonParserException e) {
return Collections.emptyList();
}
}
}

View File

@ -0,0 +1,54 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import javax.annotation.Nullable;
public class BandcampDiscographStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
private final JsonObject discograph;
public BandcampDiscographStreamInfoItemExtractor(final JsonObject discograph,
final String uploaderUrl) {
super(uploaderUrl);
this.discograph = discograph;
}
@Override
public String getUploaderName() {
return discograph.getString("band_name");
}
@Nullable
@Override
public String getUploaderAvatarUrl() {
return null;
}
@Override
public String getName() {
return discograph.getString("title");
}
@Override
public String getUrl() throws ParsingException {
return BandcampExtractorHelper.getStreamUrlFromIds(
discograph.getLong("band_id"),
discograph.getLong("item_id"),
discograph.getString("item_type")
);
}
@Override
public String getThumbnailUrl() throws ParsingException {
return BandcampExtractorHelper.getImageUrl(
discograph.getLong("art_id"), true
);
}
@Override
public long getDuration() {
return -1;
}
}

View File

@ -0,0 +1,83 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
import com.grack.nanojson.JsonObject;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.stream.StreamExtractor;
import javax.annotation.Nullable;
import java.io.IOException;
public class BandcampPlaylistStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
private final JsonObject track;
private String substituteCoverUrl;
private final StreamingService service;
public BandcampPlaylistStreamInfoItemExtractor(final JsonObject track,
final String uploaderUrl,
final StreamingService service) {
super(uploaderUrl);
this.track = track;
this.service = service;
}
public BandcampPlaylistStreamInfoItemExtractor(final JsonObject track,
final String uploaderUrl,
final String substituteCoverUrl) {
this(track, uploaderUrl, (StreamingService) null);
this.substituteCoverUrl = substituteCoverUrl;
}
@Override
public String getName() {
return track.getString("title");
}
@Override
public String getUrl() {
return getUploaderUrl() + track.getString("title_link");
}
@Override
public long getDuration() {
return track.getLong("duration");
}
@Override
public String getUploaderName() {
/* Tracks can have an individual artist name, but it is not included in the
* given JSON.
*/
return "";
}
@Nullable
@Override
public String getUploaderAvatarUrl() {
return null;
}
/**
* Each track can have its own cover art. Therefore, unless a substitute is provided,
* the thumbnail is extracted using a stream extractor.
*/
@Override
public String getThumbnailUrl() throws ParsingException {
if (substituteCoverUrl != null) {
return substituteCoverUrl;
} else {
try {
final StreamExtractor extractor = service.getStreamExtractor(getUrl());
extractor.fetchPage();
return extractor.getThumbnailUrl();
} catch (final ExtractionException | IOException e) {
throw new ParsingException("could not download cover art location", e);
}
}
}
}

View File

@ -0,0 +1,57 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
import org.jsoup.nodes.Element;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import javax.annotation.Nullable;
public class BandcampSearchStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
private final Element resultInfo;
private final Element searchResult;
public BandcampSearchStreamInfoItemExtractor(final Element searchResult,
final String uploaderUrl) {
super(uploaderUrl);
this.searchResult = searchResult;
resultInfo = searchResult.getElementsByClass("result-info").first();
}
@Override
public String getUploaderName() {
final String subhead = resultInfo.getElementsByClass("subhead").text();
final String[] splitBy = subhead.split("by ");
if (splitBy.length > 1) {
return splitBy[1];
} else {
return splitBy[0];
}
}
@Nullable
@Override
public String getUploaderAvatarUrl() {
return null;
}
@Override
public String getName() throws ParsingException {
return resultInfo.getElementsByClass("heading").text();
}
@Override
public String getUrl() throws ParsingException {
return resultInfo.getElementsByClass("itemurl").text();
}
@Override
public String getThumbnailUrl() throws ParsingException {
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
}
@Override
public long getDuration() {
return -1;
}
}

View File

@ -0,0 +1,56 @@
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.localization.DateWrapper;
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
import org.schabi.newpipe.extractor.stream.StreamType;
import javax.annotation.Nullable;
/**
* Implements methods that return a constant value in subclasses for better readability.
*/
public abstract class BandcampStreamInfoItemExtractor implements StreamInfoItemExtractor {
private final String uploaderUrl;
public BandcampStreamInfoItemExtractor(final String uploaderUrl) {
this.uploaderUrl = uploaderUrl;
}
@Override
public StreamType getStreamType() {
return StreamType.AUDIO_STREAM;
}
@Override
public long getViewCount() {
return -1;
}
@Override
public String getUploaderUrl() {
return uploaderUrl;
}
@Nullable
@Override
public String getTextualUploadDate() {
return null;
}
@Nullable
@Override
public DateWrapper getUploadDate() {
return null;
}
@Override
public boolean isUploaderVerified() throws ParsingException {
return false;
}
@Override
public boolean isAd() {
return false;
}
}

View File

@ -0,0 +1,90 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import java.io.IOException;
import java.util.List;
/**
* Artist do have IDs that are useful
*/
public class BandcampChannelLinkHandlerFactory extends ListLinkHandlerFactory {
@Override
public String getId(final String url) throws ParsingException {
try {
final String response = NewPipe.getDownloader().get(url).responseBody();
// Use band data embedded in website to extract ID
final JsonObject bandData = JsonUtils.getJsonData(response, "data-band");
return String.valueOf(bandData.getLong("id"));
} catch (final IOException | ReCaptchaException | ArrayIndexOutOfBoundsException
| JsonParserException e) {
throw new ParsingException("Download failed", e);
}
}
/**
* Uses the mobile endpoint as a "translator" from id to url
*/
@Override
public String getUrl(final String id, final List<String> contentFilter, final String sortFilter)
throws ParsingException {
try {
return BandcampExtractorHelper.getArtistDetails(id)
.getString("bandcamp_url")
.replace("http://", "https://");
} catch (final NullPointerException e) {
throw new ParsingException(
"JSON does not contain URL (invalid id?) or is otherwise invalid", e);
}
}
/**
* Accepts only pages that lead to the root of an artist profile. Supports external pages.
*/
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
final String lowercaseUrl = url.toLowerCase();
// https: | | artist.bandcamp.com | releases
// 0 1 2 3
final String[] splitUrl = lowercaseUrl.split("/");
// URL is too short
if (splitUrl.length < 3) {
return false;
}
// Must have "releases" or "music" as segment after url or none at all
if (splitUrl.length > 3 && !(
splitUrl[3].equals("releases") || splitUrl[3].equals("music")
)) {
return false;
} else {
if (splitUrl[2].equals("daily.bandcamp.com")) {
// Refuse links to daily.bandcamp.com as that is not an artist
return false;
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(lowercaseUrl);
}
}
}

View File

@ -0,0 +1,37 @@
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import java.util.List;
/**
* Like in {@link BandcampStreamLinkHandlerFactory}, tracks have no meaningful IDs except for
* their URLs
*/
public class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFactory {
@Override
public String getId(final String url) throws ParsingException {
return url;
}
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
// Don't accept URLs that don't point to a track
if (!url.toLowerCase().matches("https?://.+\\..+/(track|album)/.+")) {
return false;
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(url);
}
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
return id;
}
}

View File

@ -0,0 +1,50 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import java.util.List;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.FEATURED_API_URL;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.KIOSK_FEATURED;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.KIOSK_RADIO;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.RADIO_API_URL;
public class BandcampFeaturedLinkHandlerFactory extends ListLinkHandlerFactory {
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter) {
if (id.equals(KIOSK_FEATURED)) {
return FEATURED_API_URL; // doesn't have a website
} else if (id.equals(KIOSK_RADIO)) {
return RADIO_API_URL; // doesn't have its own website
} else {
return null;
}
}
@Override
public String getId(final String url) {
final String fixedUrl = Utils.replaceHttpWithHttps(url);
if (BandcampExtractorHelper.isRadioUrl(fixedUrl) || fixedUrl.equals(RADIO_API_URL)) {
return KIOSK_RADIO;
} else if (fixedUrl.equals(FEATURED_API_URL)) {
return KIOSK_FEATURED;
} else {
return null;
}
}
@Override
public boolean onAcceptUrl(final String url) {
final String fixedUrl = Utils.replaceHttpWithHttps(url);
return fixedUrl.equals(FEATURED_API_URL)
|| fixedUrl.equals(RADIO_API_URL)
|| BandcampExtractorHelper.isRadioUrl(fixedUrl);
}
}

View File

@ -0,0 +1,41 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
import java.util.List;
/**
* Just as with streams, the album ids are essentially useless for us.
*/
public class BandcampPlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
@Override
public String getId(final String url) throws ParsingException {
return getUrl(url);
}
@Override
public String getUrl(final String url,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
return url;
}
/**
* Accepts all bandcamp URLs that contain /album/ behind their domain name.
*/
@Override
public boolean onAcceptUrl(final String url) throws ParsingException {
// Exclude URLs which do not lead to an album
if (!url.toLowerCase().matches("https?://.+\\..+/album/.+")) {
return false;
}
// Test whether domain is supported
return BandcampExtractorHelper.isSupportedDomain(url);
}
}

View File

@ -0,0 +1,25 @@
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class BandcampSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
@Override
public String getUrl(final String query,
final List<String> contentFilter,
final String sortFilter) throws ParsingException {
try {
return BASE_URL + "/search?q=" + Utils.encodeUrlUtf8(query) + "&page=1";
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("query \"" + query + "\" could not be encoded", e);
}
}
}

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