new
This commit is contained in:
commit
9aab6148ba
|
@ -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.
|
|
@ -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"
|
|
@ -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/**
|
|
@ -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
|
|
@ -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
|
|
@ -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>.
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 New Issue