new
This commit is contained in:
commit
9aab6148ba
756 changed files with 155754 additions and 0 deletions
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
12
.github/dependabot.yml
vendored
Normal 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
51
.github/workflows/ci.yml
vendored
Normal 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
38
.github/workflows/docs.yml
vendored
Normal 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
26
.gitignore
vendored
Normal 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
674
LICENSE
Normal 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
57
README.md
Normal 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
83
build.gradle
Normal 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
193
checkstyle/checkstyle.xml
Normal 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
45
extractor/build.gradle
Normal 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'
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
111
extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
Normal file
111
extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
Normal 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 > 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 + "\")");
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue