new
This commit is contained in:
commit
9aab6148ba
756 changed files with 155754 additions and 0 deletions
45
extractor/build.gradle
Normal file
45
extractor/build.gradle
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
plugins {
|
||||
id 'checkstyle'
|
||||
}
|
||||
|
||||
test {
|
||||
// Pass on downloader type to tests for different CI jobs. See DownloaderFactory.java and ci.yml
|
||||
if (System.properties.containsKey('downloader')) {
|
||||
systemProperty('downloader', System.getProperty('downloader'))
|
||||
}
|
||||
useJUnitPlatform()
|
||||
dependsOn checkstyleMain // run checkstyle when testing
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
getConfigDirectory().set(rootProject.file("checkstyle"))
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion checkstyleVersion
|
||||
}
|
||||
|
||||
checkstyleTest {
|
||||
enabled false // do not checkstyle test files
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':timeago-parser')
|
||||
|
||||
implementation "com.github.TeamNewPipe:nanojson:$nanojsonVersion"
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation "com.github.spotbugs:spotbugs-annotations:$spotbugsVersion"
|
||||
|
||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||
implementation 'org.mozilla:rhino:1.7.13'
|
||||
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:$checkstyleVersion"
|
||||
|
||||
testImplementation platform("org.junit:junit-bom:$junitVersion")
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params'
|
||||
|
||||
testImplementation "com.squareup.okhttp3:okhttp:3.12.13"
|
||||
testImplementation 'com.google.code.gson:gson:2.10.1'
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Collectors are used to simplify the collection of information
|
||||
* from extractors
|
||||
* @param <I> the item type
|
||||
* @param <E> the extractor type
|
||||
*/
|
||||
public interface Collector<I, E> {
|
||||
|
||||
/**
|
||||
* Try to add an extractor to the collection
|
||||
* @param extractor the extractor to add
|
||||
*/
|
||||
void commit(E extractor);
|
||||
|
||||
/**
|
||||
* Try to extract the item from an extractor without adding it to the collection
|
||||
* @param extractor the extractor to use
|
||||
* @return the item
|
||||
* @throws ParsingException thrown if there is an error extracting the
|
||||
* <b>required</b> fields of the item.
|
||||
*/
|
||||
I extract(E extractor) throws ParsingException;
|
||||
|
||||
/**
|
||||
* Get all items
|
||||
* @return the items
|
||||
*/
|
||||
List<I> getItems();
|
||||
|
||||
/**
|
||||
* Get all errors
|
||||
* @return the errors
|
||||
*/
|
||||
List<Throwable> getErrors();
|
||||
|
||||
/**
|
||||
* Reset all collected items and errors
|
||||
*/
|
||||
void reset();
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class Extractor {
|
||||
/**
|
||||
* {@link StreamingService} currently related to this extractor.<br>
|
||||
* Useful for getting other things from a service (like the url handlers for
|
||||
* cleaning/accepting/get id from urls).
|
||||
*/
|
||||
private final StreamingService service;
|
||||
private final LinkHandler linkHandler;
|
||||
|
||||
@Nullable
|
||||
private Localization forcedLocalization = null;
|
||||
@Nullable
|
||||
private ContentCountry forcedContentCountry = null;
|
||||
|
||||
private boolean pageFetched = false;
|
||||
// called like this to prevent checkstyle errors about "hiding a field"
|
||||
private final Downloader downloader;
|
||||
|
||||
protected Extractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||
this.service = Objects.requireNonNull(service, "service is null");
|
||||
this.linkHandler = Objects.requireNonNull(linkHandler, "LinkHandler is null");
|
||||
this.downloader = Objects.requireNonNull(NewPipe.getDownloader(), "downloader is null");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The {@link LinkHandler} of the current extractor object (e.g. a ChannelExtractor
|
||||
* should return a channel url handler).
|
||||
*/
|
||||
@Nonnull
|
||||
public LinkHandler getLinkHandler() {
|
||||
return linkHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current page.
|
||||
*
|
||||
* @throws IOException if the page can not be loaded
|
||||
* @throws ExtractionException if the pages content is not understood
|
||||
*/
|
||||
public void fetchPage() throws IOException, ExtractionException {
|
||||
if (pageFetched) {
|
||||
return;
|
||||
}
|
||||
onFetchPage(downloader);
|
||||
pageFetched = true;
|
||||
}
|
||||
|
||||
protected void assertPageFetched() {
|
||||
if (!pageFetched) {
|
||||
throw new IllegalStateException("Page is not fetched. Make sure you call fetchPage()");
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isPageFetched() {
|
||||
return pageFetched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current page.
|
||||
*
|
||||
* @param downloader the downloader to use
|
||||
* @throws IOException if the page can not be loaded
|
||||
* @throws ExtractionException if the pages content is not understood
|
||||
*/
|
||||
@SuppressWarnings("HiddenField")
|
||||
public abstract void onFetchPage(@Nonnull Downloader downloader)
|
||||
throws IOException, ExtractionException;
|
||||
|
||||
@Nonnull
|
||||
public String getId() throws ParsingException {
|
||||
return linkHandler.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name
|
||||
*
|
||||
* @return the name
|
||||
* @throws ParsingException if the name cannot be extracted
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract String getName() throws ParsingException;
|
||||
|
||||
@Nonnull
|
||||
public String getOriginalUrl() throws ParsingException {
|
||||
return linkHandler.getOriginalUrl();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getUrl() throws ParsingException {
|
||||
return linkHandler.getUrl();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getBaseUrl() throws ParsingException {
|
||||
return linkHandler.getBaseUrl();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public StreamingService getService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return service.getServiceId();
|
||||
}
|
||||
|
||||
public Downloader getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Localization
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void forceLocalization(final Localization localization) {
|
||||
this.forcedLocalization = localization;
|
||||
}
|
||||
|
||||
public void forceContentCountry(final ContentCountry contentCountry) {
|
||||
this.forcedContentCountry = contentCountry;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Localization getExtractorLocalization() {
|
||||
return forcedLocalization == null ? getService().getLocalization() : forcedLocalization;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ContentCountry getExtractorContentCountry() {
|
||||
return forcedContentCountry == null ? getService().getContentCountry()
|
||||
: forcedContentCountry;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public TimeAgoParser getTimeAgoParser() {
|
||||
return getService().getTimeAgoParser(getExtractorLocalization());
|
||||
}
|
||||
}
|
||||
111
extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
Normal file
111
extractor/src/main/java/org/schabi/newpipe/extractor/Info.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Info implements Serializable {
|
||||
|
||||
private final int serviceId;
|
||||
/**
|
||||
* Id of this Info object <br>
|
||||
* e.g. Youtube: https://www.youtube.com/watch?v=RER5qCTzZ7 > RER5qCTzZ7
|
||||
*/
|
||||
private final String id;
|
||||
/**
|
||||
* Different than the {@link #originalUrl} in the sense that it <i>may</i> be set as a cleaned
|
||||
* url.
|
||||
*
|
||||
* @see LinkHandler#getUrl()
|
||||
* @see Extractor#getOriginalUrl()
|
||||
*/
|
||||
private final String url;
|
||||
/**
|
||||
* The url used to start the extraction of this {@link Info} object.
|
||||
*
|
||||
* @see Extractor#getOriginalUrl()
|
||||
*/
|
||||
private String originalUrl;
|
||||
private final String name;
|
||||
|
||||
private final List<Throwable> errors = new ArrayList<>();
|
||||
|
||||
public void addError(final Throwable throwable) {
|
||||
this.errors.add(throwable);
|
||||
}
|
||||
|
||||
public void addAllErrors(final Collection<Throwable> throwables) {
|
||||
this.errors.addAll(throwables);
|
||||
}
|
||||
|
||||
public Info(final int serviceId,
|
||||
final String id,
|
||||
final String url,
|
||||
final String originalUrl,
|
||||
final String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.originalUrl = originalUrl;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Info(final int serviceId, final LinkHandler linkHandler, final String name) {
|
||||
this(serviceId,
|
||||
linkHandler.getId(),
|
||||
linkHandler.getUrl(),
|
||||
linkHandler.getOriginalUrl(),
|
||||
name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final String ifDifferentString
|
||||
= url.equals(originalUrl) ? "" : " (originalUrl=\"" + originalUrl + "\")";
|
||||
return getClass().getSimpleName() + "[url=\"" + url + "\"" + ifDifferentString
|
||||
+ ", name=\"" + name + "\"]";
|
||||
}
|
||||
|
||||
// if you use an api and want to handle the website url
|
||||
// overriding original url is essential
|
||||
public void setOriginalUrl(final String originalUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public StreamingService getService() {
|
||||
try {
|
||||
return NewPipe.getService(serviceId);
|
||||
} catch (final ExtractionException e) {
|
||||
// this should be unreachable, as serviceId certainly refers to a valid service
|
||||
throw new RuntimeException("Info object has invalid service id", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getOriginalUrl() {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<Throwable> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 11.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* InfoItem.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public abstract class InfoItem implements Serializable {
|
||||
private final InfoType infoType;
|
||||
private final int serviceId;
|
||||
private final String url;
|
||||
private final String name;
|
||||
private String thumbnailUrl;
|
||||
|
||||
public InfoItem(final InfoType infoType,
|
||||
final int serviceId,
|
||||
final String url,
|
||||
final String name) {
|
||||
this.infoType = infoType;
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public InfoType getInfoType() {
|
||||
return infoType;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getSimpleName() + "[url=\"" + url + "\", name=\"" + name + "\"]";
|
||||
}
|
||||
|
||||
public enum InfoType {
|
||||
STREAM,
|
||||
PLAYLIST,
|
||||
CHANNEL,
|
||||
COMMENT
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
public interface InfoItemExtractor {
|
||||
String getName() throws ParsingException;
|
||||
String getUrl() throws ParsingException;
|
||||
String getThumbnailUrl() throws ParsingException;
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemsCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class InfoItemsCollector<I extends InfoItem, E extends InfoItemExtractor>
|
||||
implements Collector<I, E> {
|
||||
|
||||
private final List<I> itemList = new ArrayList<>();
|
||||
private final List<Throwable> errors = new ArrayList<>();
|
||||
private final int serviceId;
|
||||
@Nullable
|
||||
private final Comparator<I> comparator;
|
||||
|
||||
/**
|
||||
* Create a new collector with no comparator / sorting function
|
||||
* @param serviceId the service id
|
||||
*/
|
||||
public InfoItemsCollector(final int serviceId) {
|
||||
this(serviceId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new collector
|
||||
* @param serviceId the service id
|
||||
*/
|
||||
public InfoItemsCollector(final int serviceId, @Nullable final Comparator<I> comparator) {
|
||||
this.serviceId = serviceId;
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I> getItems() {
|
||||
if (comparator != null) {
|
||||
itemList.sort(comparator);
|
||||
}
|
||||
return Collections.unmodifiableList(itemList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Throwable> getErrors() {
|
||||
return Collections.unmodifiableList(errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
itemList.clear();
|
||||
errors.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error
|
||||
* @param error the error
|
||||
*/
|
||||
protected void addError(final Exception error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item
|
||||
* @param item the item
|
||||
*/
|
||||
protected void addItem(final I item) {
|
||||
itemList.add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service id
|
||||
* @return the service id
|
||||
*/
|
||||
public int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(final E extractor) {
|
||||
try {
|
||||
addItem(extract(extractor));
|
||||
} catch (final FoundAdException ae) {
|
||||
// found an ad. Maybe a debug line could be placed here
|
||||
} catch (final ParsingException e) {
|
||||
addError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
|
||||
/**
|
||||
* Base class to extractors that have a list (e.g. playlists, users).
|
||||
* @param <R> the info item type this list extractor provides
|
||||
*/
|
||||
public abstract class ListExtractor<R extends InfoItem> extends Extractor {
|
||||
/**
|
||||
* Constant that should be returned whenever
|
||||
* a list has an unknown number of items.
|
||||
*/
|
||||
public static final long ITEM_COUNT_UNKNOWN = -1;
|
||||
/**
|
||||
* Constant that should be returned whenever a list has an
|
||||
* infinite number of items. For example a YouTube mix.
|
||||
*/
|
||||
public static final long ITEM_COUNT_INFINITE = -2;
|
||||
/**
|
||||
* Constant that should be returned whenever a list
|
||||
* has an unknown number of items bigger than 100.
|
||||
*/
|
||||
public static final long ITEM_COUNT_MORE_THAN_100 = -3;
|
||||
|
||||
public ListExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link InfoItemsPage InfoItemsPage} corresponding to the initial page
|
||||
* where the items are from the initial request and the nextPage relative to it.
|
||||
*
|
||||
* @return a {@link InfoItemsPage} corresponding to the initial page
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract InfoItemsPage<R> getInitialPage() throws IOException, ExtractionException;
|
||||
|
||||
/**
|
||||
* Get a list of items corresponding to the specific requested page.
|
||||
*
|
||||
* @param page any page got from the exclusive implementation of the list extractor
|
||||
* @return a {@link InfoItemsPage} corresponding to the requested page
|
||||
* @see InfoItemsPage#getNextPage()
|
||||
*/
|
||||
public abstract InfoItemsPage<R> getPage(Page page) throws IOException, ExtractionException;
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ListLinkHandler getLinkHandler() {
|
||||
return (ListLinkHandler) super.getLinkHandler();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inner
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* A class that is used to wrap a list of gathered items and eventual errors, it
|
||||
* also contains a field that points to the next available page ({@link #nextPage}).
|
||||
* @param <T> the info item type that this page is supposed to store and provide
|
||||
*/
|
||||
public static class InfoItemsPage<T extends InfoItem> {
|
||||
private static final InfoItemsPage<InfoItem> EMPTY =
|
||||
new InfoItemsPage<>(Collections.emptyList(), null, Collections.emptyList());
|
||||
|
||||
/**
|
||||
* A convenient method that returns a representation of an empty page.
|
||||
*
|
||||
* @return a type-safe page with the list of items and errors empty and the nextPage set to
|
||||
* {@code null}.
|
||||
*/
|
||||
public static <T extends InfoItem> InfoItemsPage<T> emptyPage() {
|
||||
//noinspection unchecked
|
||||
return (InfoItemsPage<T>) EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current list of items of this page
|
||||
*/
|
||||
private final List<T> itemsList;
|
||||
|
||||
/**
|
||||
* Url pointing to the next page relative to this one
|
||||
*
|
||||
* @see ListExtractor#getPage(Page)
|
||||
* @see Page
|
||||
*/
|
||||
private final Page nextPage;
|
||||
|
||||
/**
|
||||
* Errors that happened during the extraction
|
||||
*/
|
||||
private final List<Throwable> errors;
|
||||
|
||||
public InfoItemsPage(final InfoItemsCollector<T, ?> collector, final Page nextPage) {
|
||||
this(collector.getItems(), nextPage, collector.getErrors());
|
||||
}
|
||||
|
||||
public InfoItemsPage(final List<T> itemsList,
|
||||
final Page nextPage,
|
||||
final List<Throwable> errors) {
|
||||
this.itemsList = itemsList;
|
||||
this.nextPage = nextPage;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public boolean hasNextPage() {
|
||||
return Page.isValid(nextPage);
|
||||
}
|
||||
|
||||
public List<T> getItems() {
|
||||
return itemsList;
|
||||
}
|
||||
|
||||
public Page getNextPage() {
|
||||
return nextPage;
|
||||
}
|
||||
|
||||
public List<Throwable> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class ListInfo<T extends InfoItem> extends Info {
|
||||
private List<T> relatedItems;
|
||||
private Page nextPage = null;
|
||||
private final List<String> contentFilters;
|
||||
private final String sortFilter;
|
||||
|
||||
public ListInfo(final int serviceId,
|
||||
final String id,
|
||||
final String url,
|
||||
final String originalUrl,
|
||||
final String name,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) {
|
||||
super(serviceId, id, url, originalUrl, name);
|
||||
this.contentFilters = contentFilter;
|
||||
this.sortFilter = sortFilter;
|
||||
}
|
||||
|
||||
public ListInfo(final int serviceId,
|
||||
final ListLinkHandler listUrlIdHandler,
|
||||
final String name) {
|
||||
super(serviceId, listUrlIdHandler, name);
|
||||
this.contentFilters = listUrlIdHandler.getContentFilters();
|
||||
this.sortFilter = listUrlIdHandler.getSortFilter();
|
||||
}
|
||||
|
||||
public List<T> getRelatedItems() {
|
||||
return relatedItems;
|
||||
}
|
||||
|
||||
public void setRelatedItems(final List<T> relatedItems) {
|
||||
this.relatedItems = relatedItems;
|
||||
}
|
||||
|
||||
public boolean hasNextPage() {
|
||||
return Page.isValid(nextPage);
|
||||
}
|
||||
|
||||
public Page getNextPage() {
|
||||
return nextPage;
|
||||
}
|
||||
|
||||
public void setNextPage(final Page page) {
|
||||
this.nextPage = page;
|
||||
}
|
||||
|
||||
public List<String> getContentFilters() {
|
||||
return contentFilters;
|
||||
}
|
||||
|
||||
public String getSortFilter() {
|
||||
return sortFilter;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/*
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
*
|
||||
* Copyright (c) Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
* and Adam Howard <achdisposable1@gmail.com> 2015
|
||||
*
|
||||
* MediaFormat.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Static data about various media formats support by NewPipe, eg mime type, extension
|
||||
*/
|
||||
|
||||
@SuppressWarnings("MethodParamPad") // we want the media format table below to be aligned
|
||||
public enum MediaFormat {
|
||||
// @formatter:off
|
||||
//video and audio combined formats
|
||||
// id name suffix mimeType
|
||||
MPEG_4 (0x0, "MPEG-4", "mp4", "video/mp4"),
|
||||
v3GPP (0x10, "3GPP", "3gp", "video/3gpp"),
|
||||
WEBM (0x20, "WebM", "webm", "video/webm"),
|
||||
// audio formats
|
||||
M4A (0x100, "m4a", "m4a", "audio/mp4"),
|
||||
WEBMA (0x200, "WebM", "webm", "audio/webm"),
|
||||
MP3 (0x300, "MP3", "mp3", "audio/mpeg"),
|
||||
OPUS (0x400, "opus", "opus", "audio/opus"),
|
||||
OGG (0x500, "ogg", "ogg", "audio/ogg"),
|
||||
WEBMA_OPUS(0x200, "WebM Opus", "webm", "audio/webm"),
|
||||
// subtitles formats
|
||||
VTT (0x1000, "WebVTT", "vtt", "text/vtt"),
|
||||
TTML (0x2000, "Timed Text Markup Language", "ttml", "application/ttml+xml"),
|
||||
TRANSCRIPT1(0x3000, "TranScript v1", "srv1", "text/xml"),
|
||||
TRANSCRIPT2(0x4000, "TranScript v2", "srv2", "text/xml"),
|
||||
TRANSCRIPT3(0x5000, "TranScript v3", "srv3", "text/xml"),
|
||||
SRT (0x6000, "SubRip file format", "srt", "text/srt");
|
||||
// @formatter:on
|
||||
|
||||
public final int id;
|
||||
public final String name;
|
||||
public final String suffix;
|
||||
public final String mimeType;
|
||||
|
||||
MediaFormat(final int id, final String name, final String suffix, final String mimeType) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.suffix = suffix;
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
private static <T> T getById(final int id,
|
||||
final Function<MediaFormat, T> field,
|
||||
final T orElse) {
|
||||
return Arrays.stream(MediaFormat.values())
|
||||
.filter(mediaFormat -> mediaFormat.id == id)
|
||||
.map(field)
|
||||
.findFirst()
|
||||
.orElse(orElse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the friendly name of the media format with the supplied id
|
||||
*
|
||||
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the friendly name of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.
|
||||
*/
|
||||
public static String getNameById(final int id) {
|
||||
return getById(id, MediaFormat::getName, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the file extension of the media format with the supplied id
|
||||
*
|
||||
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the file extension of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.
|
||||
*/
|
||||
public static String getSuffixById(final int id) {
|
||||
return getById(id, MediaFormat::getSuffix, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MIME type of the media format with the supplied id
|
||||
*
|
||||
* @param id the id of the media format. Currently an arbitrary, NewPipe-specific number.
|
||||
* @return the MIME type of the MediaFormat associated with this ids,
|
||||
* or an empty String if none match it.
|
||||
*/
|
||||
public static String getMimeById(final int id) {
|
||||
return getById(id, MediaFormat::getMimeType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the MediaFormat with the supplied mime type
|
||||
*
|
||||
* @return MediaFormat associated with this mime type,
|
||||
* or null if none match it.
|
||||
*/
|
||||
public static MediaFormat getFromMimeType(final String mimeType) {
|
||||
return Arrays.stream(MediaFormat.values())
|
||||
.filter(mediaFormat -> mediaFormat.mimeType.equals(mimeType))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media format by its id.
|
||||
*
|
||||
* @param id the id
|
||||
* @return the id of the media format or null.
|
||||
*/
|
||||
public static MediaFormat getFormatById(final int id) {
|
||||
return getById(id, mediaFormat -> mediaFormat, null);
|
||||
}
|
||||
|
||||
public static MediaFormat getFromSuffix(final String suffix) {
|
||||
return Arrays.stream(MediaFormat.values())
|
||||
.filter(mediaFormat -> mediaFormat.suffix.equals(suffix))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the format
|
||||
*
|
||||
* @return the name of the format
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename extension
|
||||
*
|
||||
* @return the filename extension
|
||||
*/
|
||||
public String getSuffix() {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mime type
|
||||
*
|
||||
* @return the mime type
|
||||
*/
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class MetaInfo implements Serializable {
|
||||
|
||||
private String title = "";
|
||||
private Description content;
|
||||
private List<URL> urls = new ArrayList<>();
|
||||
private List<String> urlTexts = new ArrayList<>();
|
||||
|
||||
public MetaInfo(@Nonnull final String title,
|
||||
@Nonnull final Description content,
|
||||
@Nonnull final List<URL> urls,
|
||||
@Nonnull final List<String> urlTexts) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.urls = urls;
|
||||
this.urlTexts = urlTexts;
|
||||
}
|
||||
|
||||
public MetaInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Title of the info. Can be empty.
|
||||
*/
|
||||
@Nonnull
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(@Nonnull final String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Description getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(@Nonnull final Description content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<URL> getUrls() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void setUrls(@Nonnull final List<URL> urls) {
|
||||
this.urls = urls;
|
||||
}
|
||||
|
||||
public void addUrl(@Nonnull final URL url) {
|
||||
urls.add(url);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<String> getUrlTexts() {
|
||||
return urlTexts;
|
||||
}
|
||||
|
||||
public void setUrlTexts(@Nonnull final List<String> urlTexts) {
|
||||
this.urlTexts = urlTexts;
|
||||
}
|
||||
|
||||
public void addUrlText(@Nonnull final String urlText) {
|
||||
urlTexts.add(urlText);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemsSearchCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collector that can handle many extractor types, to be used when a list contains items of
|
||||
* different types (e.g. search)
|
||||
* <p>
|
||||
* This collector can handle the following extractor types:
|
||||
* <ul>
|
||||
* <li>{@link StreamInfoItemExtractor}</li>
|
||||
* <li>{@link ChannelInfoItemExtractor}</li>
|
||||
* <li>{@link PlaylistInfoItemExtractor}</li>
|
||||
* </ul>
|
||||
* Calling {@link #extract(InfoItemExtractor)} or {@link #commit(InfoItemExtractor)} with any
|
||||
* other extractor type will raise an exception.
|
||||
*/
|
||||
public class MultiInfoItemsCollector extends InfoItemsCollector<InfoItem, InfoItemExtractor> {
|
||||
private final StreamInfoItemsCollector streamCollector;
|
||||
private final ChannelInfoItemsCollector userCollector;
|
||||
private final PlaylistInfoItemsCollector playlistCollector;
|
||||
|
||||
public MultiInfoItemsCollector(final int serviceId) {
|
||||
super(serviceId);
|
||||
streamCollector = new StreamInfoItemsCollector(serviceId);
|
||||
userCollector = new ChannelInfoItemsCollector(serviceId);
|
||||
playlistCollector = new PlaylistInfoItemsCollector(serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Throwable> getErrors() {
|
||||
final List<Throwable> errors = new ArrayList<>(super.getErrors());
|
||||
errors.addAll(streamCollector.getErrors());
|
||||
errors.addAll(userCollector.getErrors());
|
||||
errors.addAll(playlistCollector.getErrors());
|
||||
|
||||
return Collections.unmodifiableList(errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
streamCollector.reset();
|
||||
userCollector.reset();
|
||||
playlistCollector.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItem extract(final InfoItemExtractor extractor) throws ParsingException {
|
||||
// Use the corresponding collector for each item extractor type
|
||||
if (extractor instanceof StreamInfoItemExtractor) {
|
||||
return streamCollector.extract((StreamInfoItemExtractor) extractor);
|
||||
} else if (extractor instanceof ChannelInfoItemExtractor) {
|
||||
return userCollector.extract((ChannelInfoItemExtractor) extractor);
|
||||
} else if (extractor instanceof PlaylistInfoItemExtractor) {
|
||||
return playlistCollector.extract((PlaylistInfoItemExtractor) extractor);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid extractor type: " + extractor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 23.08.15.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
* NewPipe.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Provides access to streaming services supported by NewPipe.
|
||||
*/
|
||||
public final class NewPipe {
|
||||
private static Downloader downloader;
|
||||
private static Localization preferredLocalization;
|
||||
private static ContentCountry preferredContentCountry;
|
||||
|
||||
private NewPipe() {
|
||||
}
|
||||
|
||||
public static void init(final Downloader d) {
|
||||
init(d, Localization.DEFAULT);
|
||||
}
|
||||
|
||||
public static void init(final Downloader d, final Localization l) {
|
||||
init(d, l, l.getCountryCode().isEmpty()
|
||||
? ContentCountry.DEFAULT : new ContentCountry(l.getCountryCode()));
|
||||
}
|
||||
|
||||
public static void init(final Downloader d, final Localization l, final ContentCountry c) {
|
||||
downloader = d;
|
||||
preferredLocalization = l;
|
||||
preferredContentCountry = c;
|
||||
}
|
||||
|
||||
public static Downloader getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static List<StreamingService> getServices() {
|
||||
return ServiceList.all();
|
||||
}
|
||||
|
||||
public static StreamingService getService(final int serviceId) throws ExtractionException {
|
||||
return ServiceList.all().stream()
|
||||
.filter(service -> service.getServiceId() == serviceId)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ExtractionException(
|
||||
"There's no service with the id = \"" + serviceId + "\""));
|
||||
}
|
||||
|
||||
public static StreamingService getService(final String serviceName) throws ExtractionException {
|
||||
return ServiceList.all().stream()
|
||||
.filter(service -> service.getServiceInfo().getName().equals(serviceName))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ExtractionException(
|
||||
"There's no service with the name = \"" + serviceName + "\""));
|
||||
}
|
||||
|
||||
public static StreamingService getServiceByUrl(final String url) throws ExtractionException {
|
||||
for (final StreamingService service : ServiceList.all()) {
|
||||
if (service.getLinkTypeByUrl(url) != StreamingService.LinkType.NONE) {
|
||||
return service;
|
||||
}
|
||||
}
|
||||
throw new ExtractionException("No service can handle the url = \"" + url + "\"");
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Localization
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static void setupLocalization(final Localization thePreferredLocalization) {
|
||||
setupLocalization(thePreferredLocalization, null);
|
||||
}
|
||||
|
||||
public static void setupLocalization(
|
||||
final Localization thePreferredLocalization,
|
||||
@Nullable final ContentCountry thePreferredContentCountry) {
|
||||
NewPipe.preferredLocalization = thePreferredLocalization;
|
||||
|
||||
if (thePreferredContentCountry != null) {
|
||||
NewPipe.preferredContentCountry = thePreferredContentCountry;
|
||||
} else {
|
||||
NewPipe.preferredContentCountry = thePreferredLocalization.getCountryCode().isEmpty()
|
||||
? ContentCountry.DEFAULT
|
||||
: new ContentCountry(thePreferredLocalization.getCountryCode());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Localization getPreferredLocalization() {
|
||||
return preferredLocalization == null ? Localization.DEFAULT : preferredLocalization;
|
||||
}
|
||||
|
||||
public static void setPreferredLocalization(final Localization preferredLocalization) {
|
||||
NewPipe.preferredLocalization = preferredLocalization;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ContentCountry getPreferredContentCountry() {
|
||||
return preferredContentCountry == null ? ContentCountry.DEFAULT : preferredContentCountry;
|
||||
}
|
||||
|
||||
public static void setPreferredContentCountry(final ContentCountry preferredContentCountry) {
|
||||
NewPipe.preferredContentCountry = preferredContentCountry;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
public class Page implements Serializable {
|
||||
private final String url;
|
||||
private final String id;
|
||||
private final List<String> ids;
|
||||
private final Map<String, String> cookies;
|
||||
|
||||
@Nullable
|
||||
private final byte[] body;
|
||||
|
||||
public Page(final String url,
|
||||
final String id,
|
||||
final List<String> ids,
|
||||
final Map<String, String> cookies,
|
||||
@Nullable final byte[] body) {
|
||||
this.url = url;
|
||||
this.id = id;
|
||||
this.ids = ids;
|
||||
this.cookies = cookies;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public Page(final String url) {
|
||||
this(url, null, null, null, null);
|
||||
}
|
||||
|
||||
public Page(final String url, final String id) {
|
||||
this(url, id, null, null, null);
|
||||
}
|
||||
|
||||
public Page(final String url, final byte[] body) {
|
||||
this(url, null, null, null, body);
|
||||
}
|
||||
|
||||
public Page(final String url, final Map<String, String> cookies) {
|
||||
this(url, null, null, cookies, null);
|
||||
}
|
||||
|
||||
public Page(final List<String> ids) {
|
||||
this(null, null, ids, null, null);
|
||||
}
|
||||
|
||||
public Page(final List<String> ids, final Map<String, String> cookies) {
|
||||
this(null, null, ids, cookies, null);
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<String> getIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public Map<String, String> getCookies() {
|
||||
return cookies;
|
||||
}
|
||||
|
||||
public static boolean isValid(final Page page) {
|
||||
return page != null && (!isNullOrEmpty(page.getUrl())
|
||||
|| !isNullOrEmpty(page.getIds()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.BandcampService;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.MediaCCCService;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeService;
|
||||
import org.schabi.newpipe.extractor.services.soundcloud.SoundcloudService;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeService;
|
||||
import org.schabi.newpipe.extractor.services.xh.XhService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
||||
* ServiceList.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A list of supported services.
|
||||
*/
|
||||
@SuppressWarnings({"ConstantName", "InnerAssignment"}) // keep unusual names and inner assignments
|
||||
public final class ServiceList {
|
||||
private ServiceList() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
public static final YoutubeService YouTube;
|
||||
public static final SoundcloudService SoundCloud;
|
||||
public static final MediaCCCService MediaCCC;
|
||||
public static final PeertubeService PeerTube;
|
||||
public static final BandcampService Bandcamp;
|
||||
public static final XhService Xh;
|
||||
|
||||
/**
|
||||
* When creating a new service, put this service in the end of this list,
|
||||
* and give it the next free id.
|
||||
*/
|
||||
private static final List<StreamingService> SERVICES = Collections.unmodifiableList(
|
||||
Arrays.asList(
|
||||
YouTube = new YoutubeService(0),
|
||||
SoundCloud = new SoundcloudService(1),
|
||||
Bandcamp = new BandcampService(2),
|
||||
MediaCCC = new MediaCCCService(3),
|
||||
PeerTube = new PeertubeService(4),
|
||||
Xh = new XhService(5)
|
||||
));
|
||||
|
||||
/**
|
||||
* Get all the supported services.
|
||||
*
|
||||
* @return a unmodifiable list of all the supported services
|
||||
*/
|
||||
public static List<StreamingService> all() {
|
||||
return SERVICES;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
package org.schabi.newpipe.extractor;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.feed.FeedExtractor;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
|
||||
import org.schabi.newpipe.extractor.localization.TimeAgoPatternsManager;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Copyright (C) Christian Schabesberger 2018 <chris.schabesberger@mailbox.org>
|
||||
* StreamingService.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class StreamingService {
|
||||
|
||||
/**
|
||||
* This class holds meta information about the service implementation.
|
||||
*/
|
||||
public static class ServiceInfo {
|
||||
private final String name;
|
||||
|
||||
private final List<MediaCapability> mediaCapabilities;
|
||||
|
||||
/**
|
||||
* Creates a new instance of a ServiceInfo
|
||||
* @param name the name of the service
|
||||
* @param mediaCapabilities the type of media this service can handle
|
||||
*/
|
||||
public ServiceInfo(final String name, final List<MediaCapability> mediaCapabilities) {
|
||||
this.name = name;
|
||||
this.mediaCapabilities = Collections.unmodifiableList(mediaCapabilities);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public List<MediaCapability> getMediaCapabilities() {
|
||||
return mediaCapabilities;
|
||||
}
|
||||
|
||||
public enum MediaCapability {
|
||||
AUDIO, VIDEO, LIVE, COMMENTS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkType will be used to determine which type of URL you are handling, and therefore which
|
||||
* part of NewPipe should handle a certain URL.
|
||||
*/
|
||||
public enum LinkType {
|
||||
NONE,
|
||||
STREAM,
|
||||
CHANNEL,
|
||||
PLAYLIST
|
||||
}
|
||||
|
||||
private final int serviceId;
|
||||
private final ServiceInfo serviceInfo;
|
||||
|
||||
/**
|
||||
* Creates a new Streaming service.
|
||||
* If you Implement one do not set id within your implementation of this extractor, instead
|
||||
* set the id when you put the extractor into {@link ServiceList}
|
||||
* All other parameters can be set directly from the overriding constructor.
|
||||
* @param id the number of the service to identify him within the NewPipe frontend
|
||||
* @param name the name of the service
|
||||
* @param capabilities the type of media this service can handle
|
||||
*/
|
||||
public StreamingService(final int id,
|
||||
final String name,
|
||||
final List<ServiceInfo.MediaCapability> capabilities) {
|
||||
this.serviceId = id;
|
||||
this.serviceInfo = new ServiceInfo(name, capabilities);
|
||||
}
|
||||
|
||||
public final int getServiceId() {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public ServiceInfo getServiceInfo() {
|
||||
return serviceInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return serviceId + ":" + serviceInfo.getName();
|
||||
}
|
||||
|
||||
public abstract String getBaseUrl();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Url Id handler
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Must return a new instance of an implementation of LinkHandlerFactory for streams.
|
||||
* @return an instance of a LinkHandlerFactory for streams
|
||||
*/
|
||||
public abstract LinkHandlerFactory getStreamLHFactory();
|
||||
|
||||
/**
|
||||
* Must return a new instance of an implementation of ListLinkHandlerFactory for channels.
|
||||
* If support for channels is not given null must be returned.
|
||||
* @return an instance of a ListLinkHandlerFactory for channels or null
|
||||
*/
|
||||
public abstract ListLinkHandlerFactory getChannelLHFactory();
|
||||
|
||||
/**
|
||||
* Must return a new instance of an implementation of ListLinkHandlerFactory for playlists.
|
||||
* If support for playlists is not given null must be returned.
|
||||
* @return an instance of a ListLinkHandlerFactory for playlists or null
|
||||
*/
|
||||
public abstract ListLinkHandlerFactory getPlaylistLHFactory();
|
||||
|
||||
/**
|
||||
* Must return an instance of an implementation of SearchQueryHandlerFactory.
|
||||
* @return an instance of a SearchQueryHandlerFactory
|
||||
*/
|
||||
public abstract SearchQueryHandlerFactory getSearchQHFactory();
|
||||
public abstract ListLinkHandlerFactory getCommentsLHFactory();
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Extractors
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Must create a new instance of a SearchExtractor implementation.
|
||||
* @param queryHandler specifies the keyword lock for, and the filters which should be applied.
|
||||
* @return a new SearchExtractor instance
|
||||
*/
|
||||
public abstract SearchExtractor getSearchExtractor(SearchQueryHandler queryHandler);
|
||||
|
||||
/**
|
||||
* Must create a new instance of a SuggestionExtractor implementation.
|
||||
* @return a new SuggestionExtractor instance
|
||||
*/
|
||||
public abstract SuggestionExtractor getSuggestionExtractor();
|
||||
|
||||
/**
|
||||
* Outdated or obsolete. null can be returned.
|
||||
* @return just null
|
||||
*/
|
||||
public abstract SubscriptionExtractor getSubscriptionExtractor();
|
||||
|
||||
/**
|
||||
* This method decides which strategy will be chosen to fetch the feed. In YouTube, for example,
|
||||
* a separate feed exists which is lightweight and made specifically to be used like this.
|
||||
* <p>
|
||||
* In services which there's no other way to retrieve them, null should be returned.
|
||||
*
|
||||
* @return a {@link FeedExtractor} instance or null.
|
||||
*/
|
||||
@Nullable
|
||||
public FeedExtractor getFeedExtractor(final String url) throws ExtractionException {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must create a new instance of a KioskList implementation.
|
||||
* @return a new KioskList instance
|
||||
*/
|
||||
public abstract KioskList getKioskList() throws ExtractionException;
|
||||
|
||||
/**
|
||||
* Must create a new instance of a ChannelExtractor implementation.
|
||||
* @param linkHandler is pointing to the channel which should be handled by this new instance.
|
||||
* @return a new ChannelExtractor
|
||||
*/
|
||||
public abstract ChannelExtractor getChannelExtractor(ListLinkHandler linkHandler)
|
||||
throws ExtractionException;
|
||||
|
||||
/**
|
||||
* Must crete a new instance of a PlaylistExtractor implementation.
|
||||
* @param linkHandler is pointing to the playlist which should be handled by this new instance.
|
||||
* @return a new PlaylistExtractor
|
||||
*/
|
||||
public abstract PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler)
|
||||
throws ExtractionException;
|
||||
|
||||
/**
|
||||
* Must create a new instance of a StreamExtractor implementation.
|
||||
* @param linkHandler is pointing to the stream which should be handled by this new instance.
|
||||
* @return a new StreamExtractor
|
||||
*/
|
||||
public abstract StreamExtractor getStreamExtractor(LinkHandler linkHandler)
|
||||
throws ExtractionException;
|
||||
|
||||
public abstract CommentsExtractor getCommentsExtractor(ListLinkHandler linkHandler)
|
||||
throws ExtractionException;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Extractors without link handler
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public SearchExtractor getSearchExtractor(final String query,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) throws ExtractionException {
|
||||
return getSearchExtractor(getSearchQHFactory()
|
||||
.fromQuery(query, contentFilter, sortFilter));
|
||||
}
|
||||
|
||||
public ChannelExtractor getChannelExtractor(final String id,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter)
|
||||
throws ExtractionException {
|
||||
return getChannelExtractor(getChannelLHFactory()
|
||||
.fromQuery(id, contentFilter, sortFilter));
|
||||
}
|
||||
|
||||
public PlaylistExtractor getPlaylistExtractor(final String id,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter)
|
||||
throws ExtractionException {
|
||||
return getPlaylistExtractor(getPlaylistLHFactory()
|
||||
.fromQuery(id, contentFilter, sortFilter));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Short extractors overloads
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public SearchExtractor getSearchExtractor(final String query) throws ExtractionException {
|
||||
return getSearchExtractor(getSearchQHFactory().fromQuery(query));
|
||||
}
|
||||
|
||||
public ChannelExtractor getChannelExtractor(final String url) throws ExtractionException {
|
||||
return getChannelExtractor(getChannelLHFactory().fromUrl(url));
|
||||
}
|
||||
|
||||
public PlaylistExtractor getPlaylistExtractor(final String url) throws ExtractionException {
|
||||
return getPlaylistExtractor(getPlaylistLHFactory().fromUrl(url));
|
||||
}
|
||||
|
||||
public StreamExtractor getStreamExtractor(final String url) throws ExtractionException {
|
||||
return getStreamExtractor(getStreamLHFactory().fromUrl(url));
|
||||
}
|
||||
|
||||
public CommentsExtractor getCommentsExtractor(final String url) throws ExtractionException {
|
||||
final ListLinkHandlerFactory listLinkHandlerFactory = getCommentsLHFactory();
|
||||
if (listLinkHandlerFactory == null) {
|
||||
return null;
|
||||
}
|
||||
return getCommentsExtractor(listLinkHandlerFactory.fromUrl(url));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Figures out where the link is pointing to (a channel, a video, a playlist, etc.)
|
||||
* @param url the url on which it should be decided of which link type it is
|
||||
* @return the link type of url
|
||||
*/
|
||||
public final LinkType getLinkTypeByUrl(final String url) throws ParsingException {
|
||||
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||
|
||||
final LinkHandlerFactory sH = getStreamLHFactory();
|
||||
final LinkHandlerFactory cH = getChannelLHFactory();
|
||||
final LinkHandlerFactory pH = getPlaylistLHFactory();
|
||||
|
||||
if (sH != null && sH.acceptUrl(polishedUrl)) {
|
||||
return LinkType.STREAM;
|
||||
} else if (cH != null && cH.acceptUrl(polishedUrl)) {
|
||||
return LinkType.CHANNEL;
|
||||
} else if (pH != null && pH.acceptUrl(polishedUrl)) {
|
||||
return LinkType.PLAYLIST;
|
||||
} else {
|
||||
return LinkType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Localization
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Returns a list of localizations that this service supports.
|
||||
*/
|
||||
public List<Localization> getSupportedLocalizations() {
|
||||
return Collections.singletonList(Localization.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of countries that this service supports.<br>
|
||||
*/
|
||||
public List<ContentCountry> getSupportedCountries() {
|
||||
return Collections.singletonList(ContentCountry.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localization that should be used in this service. It will get which localization
|
||||
* the user prefer (using {@link NewPipe#getPreferredLocalization()}), then it will:
|
||||
* <ul>
|
||||
* <li>Check if the exactly localization is supported by this service.</li>
|
||||
* <li>If not, check if a less specific localization is available, using only the language
|
||||
* code.</li>
|
||||
* <li>Fallback to the {@link Localization#DEFAULT default} localization.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Localization getLocalization() {
|
||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||
|
||||
// Check the localization's language and country
|
||||
if (getSupportedLocalizations().contains(preferredLocalization)) {
|
||||
return preferredLocalization;
|
||||
}
|
||||
|
||||
// Fallback to the first supported language that matches the preferred language
|
||||
for (final Localization supportedLanguage : getSupportedLocalizations()) {
|
||||
if (supportedLanguage.getLanguageCode()
|
||||
.equals(preferredLocalization.getLanguageCode())) {
|
||||
return supportedLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
return Localization.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the country that should be used to fetch content in this service. It will get which
|
||||
* country the user prefer (using {@link NewPipe#getPreferredContentCountry()}), then it will:
|
||||
* <ul>
|
||||
* <li>Check if the country is supported by this service.</li>
|
||||
* <li>If not, fallback to the {@link ContentCountry#DEFAULT default} country.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public ContentCountry getContentCountry() {
|
||||
final ContentCountry preferredContentCountry = NewPipe.getPreferredContentCountry();
|
||||
|
||||
if (getSupportedCountries().contains(preferredContentCountry)) {
|
||||
return preferredContentCountry;
|
||||
}
|
||||
|
||||
return ContentCountry.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the time ago parser using the patterns related to the passed localization.
|
||||
* <br><br>
|
||||
* Just like {@link #getLocalization()}, it will also try to fallback to a less specific
|
||||
* localization if the exact one is not available/supported.
|
||||
*
|
||||
* @throws IllegalArgumentException if the localization is not supported (parsing patterns are
|
||||
* not present).
|
||||
*/
|
||||
public TimeAgoParser getTimeAgoParser(final Localization localization) {
|
||||
final TimeAgoParser targetParser = TimeAgoPatternsManager.getTimeAgoParserFor(localization);
|
||||
|
||||
if (targetParser != null) {
|
||||
return targetParser;
|
||||
}
|
||||
|
||||
if (!localization.getCountryCode().isEmpty()) {
|
||||
final Localization lessSpecificLocalization
|
||||
= new Localization(localization.getLanguageCode());
|
||||
final TimeAgoParser lessSpecificParser
|
||||
= TimeAgoPatternsManager.getTimeAgoParserFor(lessSpecificLocalization);
|
||||
|
||||
if (lessSpecificParser != null) {
|
||||
return lessSpecificParser;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Localization is not supported (\"" + localization + "\")");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package org.schabi.newpipe.extractor.channel;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 25.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class ChannelExtractor extends ListExtractor<StreamInfoItem> {
|
||||
|
||||
public static final long UNKNOWN_SUBSCRIBER_COUNT = -1;
|
||||
|
||||
public ChannelExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
public abstract String getAvatarUrl() throws ParsingException;
|
||||
public abstract String getBannerUrl() throws ParsingException;
|
||||
public abstract String getFeedUrl() throws ParsingException;
|
||||
public abstract long getSubscriberCount() throws ParsingException;
|
||||
public abstract String getDescription() throws ParsingException;
|
||||
public abstract String getParentChannelName() throws ParsingException;
|
||||
public abstract String getParentChannelUrl() throws ParsingException;
|
||||
public abstract String getParentChannelAvatarUrl() throws ParsingException;
|
||||
public abstract boolean isVerified() throws ParsingException;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
package org.schabi.newpipe.extractor.channel;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfo extends ListInfo<StreamInfoItem> {
|
||||
|
||||
public ChannelInfo(final int serviceId,
|
||||
final String id,
|
||||
final String url,
|
||||
final String originalUrl,
|
||||
final String name,
|
||||
final ListLinkHandler listLinkHandler) {
|
||||
super(serviceId, id, url, originalUrl, name, listLinkHandler.getContentFilters(),
|
||||
listLinkHandler.getSortFilter());
|
||||
}
|
||||
|
||||
public static ChannelInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||
}
|
||||
|
||||
public static ChannelInfo getInfo(final StreamingService service, final String url)
|
||||
throws IOException, ExtractionException {
|
||||
final ChannelExtractor extractor = service.getChannelExtractor(url);
|
||||
extractor.fetchPage();
|
||||
return getInfo(extractor);
|
||||
}
|
||||
|
||||
public static InfoItemsPage<StreamInfoItem> getMoreItems(final StreamingService service,
|
||||
final String url,
|
||||
final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return service.getChannelExtractor(url).getPage(page);
|
||||
}
|
||||
|
||||
public static ChannelInfo getInfo(final ChannelExtractor extractor)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
final int serviceId = extractor.getServiceId();
|
||||
final String id = extractor.getId();
|
||||
final String url = extractor.getUrl();
|
||||
final String originalUrl = extractor.getOriginalUrl();
|
||||
final String name = extractor.getName();
|
||||
|
||||
final ChannelInfo info =
|
||||
new ChannelInfo(serviceId, id, url, originalUrl, name, extractor.getLinkHandler());
|
||||
|
||||
try {
|
||||
info.setAvatarUrl(extractor.getAvatarUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setBannerUrl(extractor.getBannerUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setFeedUrl(extractor.getFeedUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
final InfoItemsPage<StreamInfoItem> itemsPage =
|
||||
ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||
info.setRelatedItems(itemsPage.getItems());
|
||||
info.setNextPage(itemsPage.getNextPage());
|
||||
|
||||
try {
|
||||
info.setSubscriberCount(extractor.getSubscriberCount());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setDescription(extractor.getDescription());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
info.setParentChannelName(extractor.getParentChannelName());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
info.setParentChannelUrl(extractor.getParentChannelUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
info.setParentChannelAvatarUrl(extractor.getParentChannelAvatarUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
info.setVerified(extractor.isVerified());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private String avatarUrl;
|
||||
private String parentChannelName;
|
||||
private String parentChannelUrl;
|
||||
private String parentChannelAvatarUrl;
|
||||
private String bannerUrl;
|
||||
private String feedUrl;
|
||||
private long subscriberCount = -1;
|
||||
private String description;
|
||||
private String[] donationLinks;
|
||||
private boolean verified;
|
||||
|
||||
public String getParentChannelName() {
|
||||
return parentChannelName;
|
||||
}
|
||||
|
||||
public void setParentChannelName(final String parentChannelName) {
|
||||
this.parentChannelName = parentChannelName;
|
||||
}
|
||||
|
||||
public String getParentChannelUrl() {
|
||||
return parentChannelUrl;
|
||||
}
|
||||
|
||||
public void setParentChannelUrl(final String parentChannelUrl) {
|
||||
this.parentChannelUrl = parentChannelUrl;
|
||||
}
|
||||
|
||||
public String getParentChannelAvatarUrl() {
|
||||
return parentChannelAvatarUrl;
|
||||
}
|
||||
|
||||
public void setParentChannelAvatarUrl(final String parentChannelAvatarUrl) {
|
||||
this.parentChannelAvatarUrl = parentChannelAvatarUrl;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(final String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public String getBannerUrl() {
|
||||
return bannerUrl;
|
||||
}
|
||||
|
||||
public void setBannerUrl(final String bannerUrl) {
|
||||
this.bannerUrl = bannerUrl;
|
||||
}
|
||||
|
||||
public String getFeedUrl() {
|
||||
return feedUrl;
|
||||
}
|
||||
|
||||
public void setFeedUrl(final String feedUrl) {
|
||||
this.feedUrl = feedUrl;
|
||||
}
|
||||
|
||||
public long getSubscriberCount() {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(final long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String[] getDonationLinks() {
|
||||
return donationLinks;
|
||||
}
|
||||
|
||||
public void setDonationLinks(final String[] donationLinks) {
|
||||
this.donationLinks = donationLinks;
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
|
||||
public void setVerified(final boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package org.schabi.newpipe.extractor.channel;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 11.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItem.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ChannelInfoItem extends InfoItem {
|
||||
|
||||
private String description;
|
||||
private long subscriberCount = -1;
|
||||
private long streamCount = -1;
|
||||
private boolean verified = false;
|
||||
|
||||
public ChannelInfoItem(final int serviceId, final String url, final String name) {
|
||||
super(InfoType.CHANNEL, serviceId, url, name);
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public long getSubscriberCount() {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(final long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
public long getStreamCount() {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(final long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
return verified;
|
||||
}
|
||||
|
||||
public void setVerified(final boolean verified) {
|
||||
this.verified = verified;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.schabi.newpipe.extractor.channel;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public interface ChannelInfoItemExtractor extends InfoItemExtractor {
|
||||
String getDescription() throws ParsingException;
|
||||
|
||||
long getSubscriberCount() throws ParsingException;
|
||||
|
||||
long getStreamCount() throws ParsingException;
|
||||
|
||||
boolean isVerified() throws ParsingException;
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package org.schabi.newpipe.extractor.channel;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.02.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* ChannelInfoItemsCollector.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public final class ChannelInfoItemsCollector
|
||||
extends InfoItemsCollector<ChannelInfoItem, ChannelInfoItemExtractor> {
|
||||
public ChannelInfoItemsCollector(final int serviceId) {
|
||||
super(serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelInfoItem extract(final ChannelInfoItemExtractor extractor)
|
||||
throws ParsingException {
|
||||
final ChannelInfoItem resultItem = new ChannelInfoItem(
|
||||
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||
|
||||
// optional information
|
||||
try {
|
||||
resultItem.setSubscriberCount(extractor.getSubscriberCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setStreamCount(extractor.getStreamCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setDescription(extractor.getDescription());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setVerified(extractor.isVerified());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
return resultItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.schabi.newpipe.extractor.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public abstract class CommentsExtractor extends ListExtractor<CommentsInfoItem> {
|
||||
|
||||
public CommentsExtractor(final StreamingService service, final ListLinkHandler uiHandler) {
|
||||
super(service, uiHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Warning: This method is experimental and may get removed in a future release.
|
||||
* @return <code>true</code> if the comments are disabled otherwise <code>false</code> (default)
|
||||
*/
|
||||
public boolean isCommentsDisabled() throws ExtractionException {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the total number of comments
|
||||
*/
|
||||
public int getCommentsCount() throws ExtractionException {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return "Comments";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package org.schabi.newpipe.extractor.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class CommentsInfo extends ListInfo<CommentsInfoItem> {
|
||||
|
||||
private CommentsInfo(
|
||||
final int serviceId,
|
||||
final ListLinkHandler listUrlIdHandler,
|
||||
final String name) {
|
||||
super(serviceId, listUrlIdHandler, name);
|
||||
}
|
||||
|
||||
public static CommentsInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||
}
|
||||
|
||||
public static CommentsInfo getInfo(final StreamingService service, final String url)
|
||||
throws ExtractionException, IOException {
|
||||
return getInfo(service.getCommentsExtractor(url));
|
||||
}
|
||||
|
||||
public static CommentsInfo getInfo(final CommentsExtractor commentsExtractor)
|
||||
throws IOException, ExtractionException {
|
||||
// for services which do not have a comments extractor
|
||||
if (commentsExtractor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
commentsExtractor.fetchPage();
|
||||
|
||||
final String name = commentsExtractor.getName();
|
||||
final int serviceId = commentsExtractor.getServiceId();
|
||||
final ListLinkHandler listUrlIdHandler = commentsExtractor.getLinkHandler();
|
||||
|
||||
final CommentsInfo commentsInfo = new CommentsInfo(serviceId, listUrlIdHandler, name);
|
||||
commentsInfo.setCommentsExtractor(commentsExtractor);
|
||||
final InfoItemsPage<CommentsInfoItem> initialCommentsPage =
|
||||
ExtractorHelper.getItemsPageOrLogError(commentsInfo, commentsExtractor);
|
||||
commentsInfo.setCommentsDisabled(commentsExtractor.isCommentsDisabled());
|
||||
commentsInfo.setRelatedItems(initialCommentsPage.getItems());
|
||||
try {
|
||||
commentsInfo.setCommentsCount(commentsExtractor.getCommentsCount());
|
||||
} catch (final Exception e) {
|
||||
commentsInfo.addError(e);
|
||||
}
|
||||
commentsInfo.setNextPage(initialCommentsPage.getNextPage());
|
||||
|
||||
return commentsInfo;
|
||||
}
|
||||
|
||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||
final CommentsInfo commentsInfo,
|
||||
final Page page) throws ExtractionException, IOException {
|
||||
return getMoreItems(NewPipe.getService(commentsInfo.getServiceId()), commentsInfo.getUrl(),
|
||||
page);
|
||||
}
|
||||
|
||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||
final StreamingService service,
|
||||
final CommentsInfo commentsInfo,
|
||||
final Page page) throws IOException, ExtractionException {
|
||||
return getMoreItems(service, commentsInfo.getUrl(), page);
|
||||
}
|
||||
|
||||
public static InfoItemsPage<CommentsInfoItem> getMoreItems(
|
||||
final StreamingService service,
|
||||
final String url,
|
||||
final Page page) throws IOException, ExtractionException {
|
||||
return service.getCommentsExtractor(url).getPage(page);
|
||||
}
|
||||
|
||||
private transient CommentsExtractor commentsExtractor;
|
||||
private boolean commentsDisabled = false;
|
||||
private int commentsCount;
|
||||
|
||||
public CommentsExtractor getCommentsExtractor() {
|
||||
return commentsExtractor;
|
||||
}
|
||||
|
||||
public void setCommentsExtractor(final CommentsExtractor commentsExtractor) {
|
||||
this.commentsExtractor = commentsExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if the comments are disabled otherwise {@code false} (default)
|
||||
* @see CommentsExtractor#isCommentsDisabled()
|
||||
*/
|
||||
public boolean isCommentsDisabled() {
|
||||
return commentsDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param commentsDisabled {@code true} if the comments are disabled otherwise {@code false}
|
||||
*/
|
||||
public void setCommentsDisabled(final boolean commentsDisabled) {
|
||||
this.commentsDisabled = commentsDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of comments.
|
||||
*
|
||||
* @return the total number of comments
|
||||
*/
|
||||
public int getCommentsCount() {
|
||||
return commentsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of comments.
|
||||
*
|
||||
* @param commentsCount the commentsCount to set.
|
||||
*/
|
||||
public void setCommentsCount(final int commentsCount) {
|
||||
this.commentsCount = commentsCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package org.schabi.newpipe.extractor.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class CommentsInfoItem extends InfoItem {
|
||||
|
||||
private String commentId;
|
||||
private Description commentText;
|
||||
private String uploaderName;
|
||||
private String uploaderAvatarUrl;
|
||||
private String uploaderUrl;
|
||||
private boolean uploaderVerified;
|
||||
private String textualUploadDate;
|
||||
@Nullable
|
||||
private DateWrapper uploadDate;
|
||||
private int likeCount;
|
||||
private String textualLikeCount;
|
||||
private boolean heartedByUploader;
|
||||
private boolean pinned;
|
||||
private int streamPosition;
|
||||
private int replyCount;
|
||||
@Nullable
|
||||
private Page replies;
|
||||
|
||||
public static final int NO_LIKE_COUNT = -1;
|
||||
public static final int NO_STREAM_POSITION = -1;
|
||||
|
||||
public static final int UNKNOWN_REPLY_COUNT = -1;
|
||||
|
||||
public CommentsInfoItem(final int serviceId, final String url, final String name) {
|
||||
super(InfoType.COMMENT, serviceId, url, name);
|
||||
}
|
||||
|
||||
public String getCommentId() {
|
||||
return commentId;
|
||||
}
|
||||
|
||||
public void setCommentId(final String commentId) {
|
||||
this.commentId = commentId;
|
||||
}
|
||||
|
||||
public Description getCommentText() {
|
||||
return commentText;
|
||||
}
|
||||
|
||||
public void setCommentText(final Description commentText) {
|
||||
this.commentText = commentText;
|
||||
}
|
||||
|
||||
public String getUploaderName() {
|
||||
return uploaderName;
|
||||
}
|
||||
|
||||
public void setUploaderName(final String uploaderName) {
|
||||
this.uploaderName = uploaderName;
|
||||
}
|
||||
|
||||
public String getUploaderAvatarUrl() {
|
||||
return uploaderAvatarUrl;
|
||||
}
|
||||
|
||||
public void setUploaderAvatarUrl(final String uploaderAvatarUrl) {
|
||||
this.uploaderAvatarUrl = uploaderAvatarUrl;
|
||||
}
|
||||
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
public void setUploaderUrl(final String uploaderUrl) {
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
}
|
||||
|
||||
public String getTextualUploadDate() {
|
||||
return textualUploadDate;
|
||||
}
|
||||
|
||||
public void setTextualUploadDate(final String textualUploadDate) {
|
||||
this.textualUploadDate = textualUploadDate;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DateWrapper getUploadDate() {
|
||||
return uploadDate;
|
||||
}
|
||||
|
||||
public void setUploadDate(@Nullable final DateWrapper uploadDate) {
|
||||
this.uploadDate = uploadDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the comment's like count
|
||||
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable
|
||||
*/
|
||||
public int getLikeCount() {
|
||||
return likeCount;
|
||||
}
|
||||
|
||||
public void setLikeCount(final int likeCount) {
|
||||
this.likeCount = likeCount;
|
||||
}
|
||||
|
||||
public String getTextualLikeCount() {
|
||||
return textualLikeCount;
|
||||
}
|
||||
|
||||
public void setTextualLikeCount(final String textualLikeCount) {
|
||||
this.textualLikeCount = textualLikeCount;
|
||||
}
|
||||
|
||||
public void setHeartedByUploader(final boolean isHeartedByUploader) {
|
||||
this.heartedByUploader = isHeartedByUploader;
|
||||
}
|
||||
|
||||
public boolean isHeartedByUploader() {
|
||||
return this.heartedByUploader;
|
||||
}
|
||||
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
public void setPinned(final boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
public void setUploaderVerified(final boolean uploaderVerified) {
|
||||
this.uploaderVerified = uploaderVerified;
|
||||
}
|
||||
|
||||
public boolean isUploaderVerified() {
|
||||
return uploaderVerified;
|
||||
}
|
||||
|
||||
public void setStreamPosition(final int streamPosition) {
|
||||
this.streamPosition = streamPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the playback position of the stream to which this comment belongs.
|
||||
* This is not supported by all services.
|
||||
*
|
||||
* @return the playback position in seconds or {@link #NO_STREAM_POSITION} if not available
|
||||
*/
|
||||
public int getStreamPosition() {
|
||||
return streamPosition;
|
||||
}
|
||||
|
||||
public void setReplyCount(final int replyCount) {
|
||||
this.replyCount = replyCount;
|
||||
}
|
||||
|
||||
public int getReplyCount() {
|
||||
return replyCount;
|
||||
}
|
||||
|
||||
public void setReplies(@Nullable final Page replies) {
|
||||
this.replies = replies;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Page getReplies() {
|
||||
return this.replies;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package org.schabi.newpipe.extractor.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeCommentsInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface CommentsInfoItemExtractor extends InfoItemExtractor {
|
||||
|
||||
/**
|
||||
* Return the like count of the comment,
|
||||
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable.
|
||||
*
|
||||
* <br>
|
||||
* <p>
|
||||
* NOTE: Currently only implemented for YT {@link
|
||||
* YoutubeCommentsInfoItemExtractor#getLikeCount()}
|
||||
* with limitations (only approximate like count is returned)
|
||||
*
|
||||
* @return the comment's like count
|
||||
* or {@link CommentsInfoItem#NO_LIKE_COUNT} if it is unavailable
|
||||
* @see StreamExtractor#getLikeCount()
|
||||
*/
|
||||
default int getLikeCount() throws ParsingException {
|
||||
return CommentsInfoItem.NO_LIKE_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unmodified like count given by the service
|
||||
* <br>
|
||||
* It may be language dependent
|
||||
*/
|
||||
default String getTextualLikeCount() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* The text of the comment
|
||||
*/
|
||||
default Description getCommentText() throws ParsingException {
|
||||
return Description.EMPTY_DESCRIPTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* The upload date given by the service, unmodified
|
||||
*
|
||||
* @see StreamExtractor#getTextualUploadDate()
|
||||
*/
|
||||
default String getTextualUploadDate() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* The upload date wrapped with DateWrapper class
|
||||
*
|
||||
* @see StreamExtractor#getUploadDate()
|
||||
*/
|
||||
@Nullable
|
||||
default DateWrapper getUploadDate() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
default String getCommentId() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
default String getUploaderUrl() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
default String getUploaderName() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
default String getUploaderAvatarUrl() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the comment has been hearted by the uploader
|
||||
*/
|
||||
default boolean isHeartedByUploader() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the comment is pinned
|
||||
*/
|
||||
default boolean isPinned() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the uploader is verified by the service
|
||||
*/
|
||||
default boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The playback position of the stream to which this comment belongs.
|
||||
*
|
||||
* @see CommentsInfoItem#getStreamPosition()
|
||||
*/
|
||||
default int getStreamPosition() throws ParsingException {
|
||||
return CommentsInfoItem.NO_STREAM_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* The count of comment replies.
|
||||
*
|
||||
* @return the count of the replies
|
||||
* or {@link CommentsInfoItem#UNKNOWN_REPLY_COUNT} if replies are not supported
|
||||
*/
|
||||
default int getReplyCount() throws ParsingException {
|
||||
return CommentsInfoItem.UNKNOWN_REPLY_COUNT;
|
||||
}
|
||||
|
||||
/**
|
||||
* The continuation page which is used to get comment replies from.
|
||||
*
|
||||
* @return the continuation Page for the replies, or null if replies are not supported
|
||||
*/
|
||||
@Nullable
|
||||
default Page getReplies() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package org.schabi.newpipe.extractor.comments;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class CommentsInfoItemsCollector
|
||||
extends InfoItemsCollector<CommentsInfoItem, CommentsInfoItemExtractor> {
|
||||
|
||||
public CommentsInfoItemsCollector(final int serviceId) {
|
||||
super(serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentsInfoItem extract(final CommentsInfoItemExtractor extractor)
|
||||
throws ParsingException {
|
||||
final CommentsInfoItem resultItem = new CommentsInfoItem(
|
||||
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||
|
||||
// optional information
|
||||
try {
|
||||
resultItem.setCommentId(extractor.getCommentId());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setCommentText(extractor.getCommentText());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploaderName(extractor.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploaderUrl(extractor.getUploaderUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setTextualUploadDate(extractor.getTextualUploadDate());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploadDate(extractor.getUploadDate());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setLikeCount(extractor.getLikeCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setTextualLikeCount(extractor.getTextualLikeCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
resultItem.setHeartedByUploader(extractor.isHeartedByUploader());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
resultItem.setPinned(extractor.isPinned());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
resultItem.setStreamPosition(extractor.getStreamPosition());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
resultItem.setReplyCount(extractor.getReplyCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
resultItem.setReplies(extractor.getReplies());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
|
||||
return resultItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(final CommentsInfoItemExtractor extractor) {
|
||||
try {
|
||||
addItem(extract(extractor));
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<CommentsInfoItem> getCommentsInfoItemList() {
|
||||
return new ArrayList<>(super.getItems());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
package org.schabi.newpipe.extractor.downloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A base for downloader implementations that NewPipe will use
|
||||
* to download needed resources during extraction.
|
||||
*/
|
||||
public abstract class Downloader {
|
||||
|
||||
/**
|
||||
* Do a GET request to get the resource that the url is pointing to.<br>
|
||||
* <br>
|
||||
* This method calls {@link #get(String, Map, Localization)} with the default preferred
|
||||
* localization. It should only be used when the resource that will be fetched won't be affected
|
||||
* by the localization.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @return the result of the GET request
|
||||
*/
|
||||
public Response get(final String url) throws IOException, ReCaptchaException {
|
||||
return get(url, null, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a GET request to get the resource that the url is pointing to.<br>
|
||||
* <br>
|
||||
* It will set the {@code Accept-Language} header to the language of the localization parameter.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the GET request
|
||||
*/
|
||||
public Response get(final String url, final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return get(url, null, localization);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a GET request with the specified headers.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @return the result of the GET request
|
||||
*/
|
||||
public Response get(final String url, @Nullable final Map<String, List<String>> headers)
|
||||
throws IOException, ReCaptchaException {
|
||||
return get(url, headers, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a GET request with the specified headers.<br>
|
||||
* <br>
|
||||
* It will set the {@code Accept-Language} header to the language of the localization parameter.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the GET request
|
||||
*/
|
||||
public Response get(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return execute(Request.newBuilder()
|
||||
.get(url)
|
||||
.headers(headers)
|
||||
.localization(localization)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a HEAD request.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @return the result of the HEAD request
|
||||
*/
|
||||
public Response head(final String url) throws IOException, ReCaptchaException {
|
||||
return head(url, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a HEAD request with the specified headers.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @return the result of the HEAD request
|
||||
*/
|
||||
public Response head(final String url, @Nullable final Map<String, List<String>> headers)
|
||||
throws IOException, ReCaptchaException {
|
||||
return execute(Request.newBuilder()
|
||||
.head(url)
|
||||
.headers(headers)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a POST request with the specified headers, sending the data array.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @return the result of the POST request
|
||||
*/
|
||||
public Response post(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend)
|
||||
throws IOException, ReCaptchaException {
|
||||
return post(url, headers, dataToSend, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a POST request with the specified headers, sending the data array.
|
||||
* <br>
|
||||
* It will set the {@code Accept-Language} header to the language of the localization parameter.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the POST request
|
||||
*/
|
||||
public Response post(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return execute(Request.newBuilder()
|
||||
.post(url, dataToSend)
|
||||
.headers(headers)
|
||||
.localization(localization)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request using the specified value of the
|
||||
* {@code Content-Type} header with a given {@link Localization}.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||
* {@code Content-Type} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentType(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final Localization localization,
|
||||
final String contentType)
|
||||
throws IOException, ReCaptchaException {
|
||||
final Map<String, List<String>> actualHeaders = new HashMap<>();
|
||||
if (headers != null) {
|
||||
actualHeaders.putAll(headers);
|
||||
}
|
||||
actualHeaders.put("Content-Type", Collections.singletonList(contentType));
|
||||
return post(url, actualHeaders, dataToSend, localization);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request using the specified value of the
|
||||
* {@code Content-Type} header.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param contentType the mime type of the body sent, which will be set as the value of the
|
||||
* {@code Content-Type} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentType(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final String contentType)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentType(url, headers, dataToSend, NewPipe.getPreferredLocalization(),
|
||||
contentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||
* {@code Content-Type} header with a given {@link Localization}.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @param localization the source of the value of the {@code Accept-Language} header
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentTypeJson(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
final Localization localization)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentType(url, headers, dataToSend, localization, "application/json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenient method to send a POST request the JSON mime type as the value of the
|
||||
* {@code Content-Type} header.
|
||||
*
|
||||
* @param url the URL that is pointing to the wanted resource
|
||||
* @param headers a list of headers that will be used in the request.
|
||||
* Any default headers <b>should</b> be overridden by these.
|
||||
* @param dataToSend byte array that will be sent when doing the request.
|
||||
* @return the result of the POST request
|
||||
* @see #post(String, Map, byte[], Localization)
|
||||
*/
|
||||
public Response postWithContentTypeJson(final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend)
|
||||
throws IOException, ReCaptchaException {
|
||||
return postWithContentTypeJson(url, headers, dataToSend,
|
||||
NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a request using the specified {@link Request} object.
|
||||
*
|
||||
* @return the result of the request
|
||||
*/
|
||||
public abstract Response execute(@Nonnull Request request)
|
||||
throws IOException, ReCaptchaException;
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
package org.schabi.newpipe.extractor.downloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* An object that holds request information used when {@link Downloader#execute(Request) executing}
|
||||
* a request.
|
||||
*/
|
||||
public class Request {
|
||||
private final String httpMethod;
|
||||
private final String url;
|
||||
private final Map<String, List<String>> headers;
|
||||
@Nullable
|
||||
private final byte[] dataToSend;
|
||||
@Nullable
|
||||
private final Localization localization;
|
||||
|
||||
public Request(final String httpMethod,
|
||||
final String url,
|
||||
@Nullable final Map<String, List<String>> headers,
|
||||
@Nullable final byte[] dataToSend,
|
||||
@Nullable final Localization localization,
|
||||
final boolean automaticLocalizationHeader) {
|
||||
this.httpMethod = Objects.requireNonNull(httpMethod, "Request's httpMethod is null");
|
||||
this.url = Objects.requireNonNull(url, "Request's url is null");
|
||||
this.dataToSend = dataToSend;
|
||||
this.localization = localization;
|
||||
|
||||
final Map<String, List<String>> actualHeaders = new LinkedHashMap<>();
|
||||
if (headers != null) {
|
||||
actualHeaders.putAll(headers);
|
||||
}
|
||||
if (automaticLocalizationHeader && localization != null) {
|
||||
actualHeaders.putAll(getHeadersFromLocalization(localization));
|
||||
}
|
||||
|
||||
this.headers = Collections.unmodifiableMap(actualHeaders);
|
||||
}
|
||||
|
||||
private Request(final Builder builder) {
|
||||
this(builder.httpMethod, builder.url, builder.headers, builder.dataToSend,
|
||||
builder.localization, builder.automaticLocalizationHeader);
|
||||
}
|
||||
|
||||
/**
|
||||
* A http method (i.e. {@code GET, POST, HEAD}).
|
||||
*/
|
||||
public String httpMethod() {
|
||||
return httpMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL that is pointing to the wanted resource.
|
||||
*/
|
||||
public String url() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of headers that will be used in the request.<br>
|
||||
* Any default headers that the implementation may have, <b>should</b> be overridden by these.
|
||||
*/
|
||||
public Map<String, List<String>> headers() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional byte array that will be sent when doing the request, very commonly used in
|
||||
* {@code POST} requests.<br>
|
||||
* <br>
|
||||
* The implementation should make note of some recommended headers
|
||||
* (for example, {@code Content-Length} in a post request).
|
||||
*/
|
||||
@Nullable
|
||||
public byte[] dataToSend() {
|
||||
return dataToSend;
|
||||
}
|
||||
|
||||
/**
|
||||
* A localization object that should be used when executing a request.<br>
|
||||
* <br>
|
||||
* Usually the {@code Accept-Language} will be set to this value (a helper
|
||||
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
|
||||
*/
|
||||
@Nullable
|
||||
public Localization localization() {
|
||||
return localization;
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String httpMethod;
|
||||
private String url;
|
||||
private final Map<String, List<String>> headers = new LinkedHashMap<>();
|
||||
private byte[] dataToSend;
|
||||
private Localization localization;
|
||||
private boolean automaticLocalizationHeader = true;
|
||||
|
||||
public Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A http method (i.e. {@code GET, POST, HEAD}).
|
||||
*/
|
||||
public Builder httpMethod(final String httpMethodToSet) {
|
||||
this.httpMethod = httpMethodToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL that is pointing to the wanted resource.
|
||||
*/
|
||||
public Builder url(final String urlToSet) {
|
||||
this.url = urlToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of headers that will be used in the request.<br>
|
||||
* Any default headers that the implementation may have, <b>should</b> be overridden by
|
||||
* these.
|
||||
*/
|
||||
public Builder headers(@Nullable final Map<String, List<String>> headersToSet) {
|
||||
this.headers.clear();
|
||||
if (headersToSet != null) {
|
||||
this.headers.putAll(headersToSet);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional byte array that will be sent when doing the request, very commonly used in
|
||||
* {@code POST} requests.<br>
|
||||
* <br>
|
||||
* The implementation should make note of some recommended headers
|
||||
* (for example, {@code Content-Length} in a post request).
|
||||
*/
|
||||
public Builder dataToSend(final byte[] dataToSendToSet) {
|
||||
this.dataToSend = dataToSendToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A localization object that should be used when executing a request.<br>
|
||||
* <br>
|
||||
* Usually the {@code Accept-Language} will be set to this value (a helper
|
||||
* method to do this easily: {@link Request#getHeadersFromLocalization(Localization)}).
|
||||
*/
|
||||
public Builder localization(final Localization localizationToSet) {
|
||||
this.localization = localizationToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If localization headers should automatically be included in the request.
|
||||
*/
|
||||
public Builder automaticLocalizationHeader(final boolean automaticLocalizationHeaderToSet) {
|
||||
this.automaticLocalizationHeader = automaticLocalizationHeaderToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Request build() {
|
||||
return new Request(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Http Methods Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public Builder get(final String urlToSet) {
|
||||
this.httpMethod = "GET";
|
||||
this.url = urlToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder head(final String urlToSet) {
|
||||
this.httpMethod = "HEAD";
|
||||
this.url = urlToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder post(final String urlToSet, @Nullable final byte[] dataToSendToSet) {
|
||||
this.httpMethod = "POST";
|
||||
this.url = urlToSet;
|
||||
this.dataToSend = dataToSendToSet;
|
||||
return this;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Additional Headers Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public Builder setHeaders(final String headerName, final List<String> headerValueList) {
|
||||
this.headers.remove(headerName);
|
||||
this.headers.put(headerName, headerValueList);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addHeaders(final String headerName, final List<String> headerValueList) {
|
||||
@Nullable List<String> currentHeaderValueList = this.headers.get(headerName);
|
||||
if (currentHeaderValueList == null) {
|
||||
currentHeaderValueList = new ArrayList<>();
|
||||
}
|
||||
|
||||
currentHeaderValueList.addAll(headerValueList);
|
||||
this.headers.put(headerName, headerValueList);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHeader(final String headerName, final String headerValue) {
|
||||
return setHeaders(headerName, Collections.singletonList(headerValue));
|
||||
}
|
||||
|
||||
public Builder addHeader(final String headerName, final String headerValue) {
|
||||
return addHeaders(headerName, Collections.singletonList(headerValue));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Nonnull
|
||||
public static Map<String, List<String>> getHeadersFromLocalization(
|
||||
@Nullable final Localization localization) {
|
||||
if (localization == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final String languageCode = localization.getLanguageCode();
|
||||
final List<String> languageCodeList = Collections.singletonList(
|
||||
localization.getCountryCode().isEmpty() ? languageCode
|
||||
: localization.getLocalizationCode() + ", " + languageCode + ";q=0.9");
|
||||
return Collections.singletonMap("Accept-Language", languageCodeList);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Generated
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
final Request request = (Request) o;
|
||||
return httpMethod.equals(request.httpMethod)
|
||||
&& url.equals(request.url)
|
||||
&& headers.equals(request.headers)
|
||||
&& Arrays.equals(dataToSend, request.dataToSend)
|
||||
&& Objects.equals(localization, request.localization);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(httpMethod, url, headers, localization);
|
||||
result = 31 * result + Arrays.hashCode(dataToSend);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package org.schabi.newpipe.extractor.downloader;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A Data class used to hold the results from requests made by the Downloader implementation.
|
||||
*/
|
||||
public class Response {
|
||||
private final int responseCode;
|
||||
private final String responseMessage;
|
||||
private final Map<String, List<String>> responseHeaders;
|
||||
private final String responseBody;
|
||||
|
||||
private final String latestUrl;
|
||||
|
||||
public Response(final int responseCode,
|
||||
final String responseMessage,
|
||||
@Nullable final Map<String, List<String>> responseHeaders,
|
||||
@Nullable final String responseBody,
|
||||
@Nullable final String latestUrl) {
|
||||
this.responseCode = responseCode;
|
||||
this.responseMessage = responseMessage;
|
||||
this.responseHeaders = responseHeaders == null ? Collections.emptyMap() : responseHeaders;
|
||||
|
||||
this.responseBody = responseBody == null ? "" : responseBody;
|
||||
this.latestUrl = latestUrl;
|
||||
}
|
||||
|
||||
public int responseCode() {
|
||||
return responseCode;
|
||||
}
|
||||
|
||||
public String responseMessage() {
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> responseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String responseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for detecting a possible redirection, limited to the latest one.
|
||||
*
|
||||
* @return latest url known right before this response object was created
|
||||
*/
|
||||
@Nonnull
|
||||
public String latestUrl() {
|
||||
return latestUrl;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* For easy access to some header value that (usually) don't repeat itself.
|
||||
* <p>For getting all the values associated to the header, use {@link #responseHeaders()} (e.g.
|
||||
* {@code Set-Cookie}).
|
||||
*
|
||||
* @param name the name of the header
|
||||
* @return the first value assigned to this header
|
||||
*/
|
||||
@Nullable
|
||||
public String getHeader(final String name) {
|
||||
for (final Map.Entry<String, List<String>> headerEntry : responseHeaders.entrySet()) {
|
||||
final String key = headerEntry.getKey();
|
||||
if (key != null && key.equalsIgnoreCase(name) && !headerEntry.getValue().isEmpty()) {
|
||||
return headerEntry.getValue().get(0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class AccountTerminatedException extends ContentNotAvailableException {
|
||||
|
||||
private Reason reason = Reason.UNKNOWN;
|
||||
|
||||
public AccountTerminatedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AccountTerminatedException(final String message, final Reason reason) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public AccountTerminatedException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason for the violation. There should also be more info in the exception's message.
|
||||
*/
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
UNKNOWN,
|
||||
VIOLATION
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class AgeRestrictedContentException extends ContentNotAvailableException {
|
||||
public AgeRestrictedContentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AgeRestrictedContentException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class ContentNotAvailableException extends ParsingException {
|
||||
public ContentNotAvailableException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ContentNotAvailableException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class ContentNotSupportedException extends ParsingException {
|
||||
public ContentNotSupportedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ContentNotSupportedException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 30.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ExtractionException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ExtractionException extends Exception {
|
||||
public ExtractionException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ExtractionException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ExtractionException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.09.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* FoundAdException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class FoundAdException extends ParsingException {
|
||||
public FoundAdException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FoundAdException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class GeographicRestrictionException extends ContentNotAvailableException {
|
||||
public GeographicRestrictionException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public GeographicRestrictionException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class PaidContentException extends ContentNotAvailableException {
|
||||
public PaidContentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PaidContentException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 31.01.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ParsingException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
public class ParsingException extends ExtractionException {
|
||||
public ParsingException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ParsingException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class PrivateContentException extends ContentNotAvailableException {
|
||||
public PrivateContentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PrivateContentException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 07.12.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* ReCaptchaException.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class ReCaptchaException extends ExtractionException {
|
||||
private final String url;
|
||||
|
||||
public ReCaptchaException(final String message, final String url) {
|
||||
super(message);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class SoundCloudGoPlusContentException extends ContentNotAvailableException {
|
||||
public SoundCloudGoPlusContentException() {
|
||||
super("This track is a SoundCloud Go+ track");
|
||||
}
|
||||
|
||||
public SoundCloudGoPlusContentException(final Throwable cause) {
|
||||
super("This track is a SoundCloud Go+ track", cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.schabi.newpipe.extractor.exceptions;
|
||||
|
||||
public class YoutubeMusicPremiumContentException extends ContentNotAvailableException {
|
||||
public YoutubeMusicPremiumContentException() {
|
||||
super("This video is a YouTube Music Premium video");
|
||||
}
|
||||
|
||||
public YoutubeMusicPremiumContentException(final Throwable cause) {
|
||||
super("This video is a YouTube Music Premium video", cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package org.schabi.newpipe.extractor.feed;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
/**
|
||||
* This class helps to extract items from lightweight feeds that the services may provide.
|
||||
* <p>
|
||||
* YouTube is an example of a service that has this alternative available.
|
||||
*/
|
||||
public abstract class FeedExtractor extends ListExtractor<StreamInfoItem> {
|
||||
public FeedExtractor(final StreamingService service, final ListLinkHandler listLinkHandler) {
|
||||
super(service, listLinkHandler);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package org.schabi.newpipe.extractor.feed;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class FeedInfo extends ListInfo<StreamInfoItem> {
|
||||
|
||||
public FeedInfo(final int serviceId,
|
||||
final String id,
|
||||
final String url,
|
||||
final String originalUrl,
|
||||
final String name,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) {
|
||||
super(serviceId, id, url, originalUrl, name, contentFilter, sortFilter);
|
||||
}
|
||||
|
||||
public static FeedInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||
}
|
||||
|
||||
public static FeedInfo getInfo(final StreamingService service, final String url)
|
||||
throws IOException, ExtractionException {
|
||||
final FeedExtractor extractor = service.getFeedExtractor(url);
|
||||
|
||||
if (extractor == null) {
|
||||
throw new IllegalArgumentException("Service \"" + service.getServiceInfo().getName()
|
||||
+ "\" doesn't support FeedExtractor.");
|
||||
}
|
||||
|
||||
extractor.fetchPage();
|
||||
return getInfo(extractor);
|
||||
}
|
||||
|
||||
public static FeedInfo getInfo(final FeedExtractor extractor)
|
||||
throws IOException, ExtractionException {
|
||||
extractor.fetchPage();
|
||||
|
||||
final int serviceId = extractor.getServiceId();
|
||||
final String id = extractor.getId();
|
||||
final String url = extractor.getUrl();
|
||||
final String originalUrl = extractor.getOriginalUrl();
|
||||
final String name = extractor.getName();
|
||||
|
||||
final FeedInfo info = new FeedInfo(serviceId, id, url, originalUrl, name, null, null);
|
||||
|
||||
final InfoItemsPage<StreamInfoItem> itemsPage
|
||||
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||
info.setRelatedItems(itemsPage.getItems());
|
||||
info.setNextPage(itemsPage.getNextPage());
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package org.schabi.newpipe.extractor.kiosk;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.08.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* KioskExtractor.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public abstract class KioskExtractor<T extends InfoItem> extends ListExtractor<T> {
|
||||
private final String id;
|
||||
|
||||
public KioskExtractor(final StreamingService streamingService,
|
||||
final ListLinkHandler linkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, linkHandler);
|
||||
this.id = kioskId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Id should be the name of the kiosk, tho Id is used for identifying it in the frontend,
|
||||
* so id should be kept in english.
|
||||
* In order to get the name of the kiosk in the desired language we have to
|
||||
* crawl if from the website.
|
||||
* @return the translated version of id
|
||||
*/
|
||||
@Nonnull
|
||||
@Override
|
||||
public abstract String getName() throws ParsingException;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package org.schabi.newpipe.extractor.kiosk;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 12.08.17.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* KioskInfo.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class KioskInfo extends ListInfo<StreamInfoItem> {
|
||||
private KioskInfo(final int serviceId, final ListLinkHandler linkHandler, final String name) {
|
||||
super(serviceId, linkHandler, name);
|
||||
}
|
||||
|
||||
public static ListExtractor.InfoItemsPage<StreamInfoItem> getMoreItems(
|
||||
final StreamingService service, final String url, final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return service.getKioskList().getExtractorByUrl(url, page).getPage(page);
|
||||
}
|
||||
|
||||
public static KioskInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||
}
|
||||
|
||||
public static KioskInfo getInfo(final StreamingService service, final String url)
|
||||
throws IOException, ExtractionException {
|
||||
final KioskExtractor extractor = service.getKioskList().getExtractorByUrl(url, null);
|
||||
extractor.fetchPage();
|
||||
return getInfo(extractor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KioskInfo from KioskExtractor
|
||||
*
|
||||
* @param extractor an extractor where fetchPage() was already got called on.
|
||||
*/
|
||||
public static KioskInfo getInfo(final KioskExtractor extractor) throws ExtractionException {
|
||||
|
||||
final KioskInfo info = new KioskInfo(extractor.getServiceId(),
|
||||
extractor.getLinkHandler(),
|
||||
extractor.getName());
|
||||
|
||||
final ListExtractor.InfoItemsPage<StreamInfoItem> itemsPage
|
||||
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||
info.setRelatedItems(itemsPage.getItems());
|
||||
info.setNextPage(itemsPage.getNextPage());
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package org.schabi.newpipe.extractor.kiosk;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry;
|
||||
import org.schabi.newpipe.extractor.localization.Localization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class KioskList {
|
||||
|
||||
public interface KioskExtractorFactory {
|
||||
KioskExtractor createNewKiosk(StreamingService streamingService,
|
||||
String url,
|
||||
String kioskId)
|
||||
throws ExtractionException, IOException;
|
||||
}
|
||||
|
||||
private final StreamingService service;
|
||||
private final HashMap<String, KioskEntry> kioskList = new HashMap<>();
|
||||
private String defaultKiosk = null;
|
||||
|
||||
@Nullable
|
||||
private Localization forcedLocalization;
|
||||
@Nullable
|
||||
private ContentCountry forcedContentCountry;
|
||||
|
||||
private static class KioskEntry {
|
||||
KioskEntry(final KioskExtractorFactory ef, final ListLinkHandlerFactory h) {
|
||||
extractorFactory = ef;
|
||||
handlerFactory = h;
|
||||
}
|
||||
|
||||
final KioskExtractorFactory extractorFactory;
|
||||
final ListLinkHandlerFactory handlerFactory;
|
||||
}
|
||||
|
||||
public KioskList(final StreamingService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public void addKioskEntry(final KioskExtractorFactory extractorFactory,
|
||||
final ListLinkHandlerFactory handlerFactory,
|
||||
final String id)
|
||||
throws Exception {
|
||||
if (kioskList.get(id) != null) {
|
||||
throw new Exception("Kiosk with type " + id + " already exists.");
|
||||
}
|
||||
kioskList.put(id, new KioskEntry(extractorFactory, handlerFactory));
|
||||
}
|
||||
|
||||
public void setDefaultKiosk(final String kioskType) {
|
||||
defaultKiosk = kioskType;
|
||||
}
|
||||
|
||||
public KioskExtractor getDefaultKioskExtractor()
|
||||
throws ExtractionException, IOException {
|
||||
return getDefaultKioskExtractor(null);
|
||||
}
|
||||
|
||||
public KioskExtractor getDefaultKioskExtractor(final Page nextPage)
|
||||
throws ExtractionException, IOException {
|
||||
return getDefaultKioskExtractor(nextPage, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
public KioskExtractor getDefaultKioskExtractor(final Page nextPage,
|
||||
final Localization localization)
|
||||
throws ExtractionException, IOException {
|
||||
if (!isNullOrEmpty(defaultKiosk)) {
|
||||
return getExtractorById(defaultKiosk, nextPage, localization);
|
||||
} else {
|
||||
final String first = kioskList.keySet().stream().findAny().orElse(null);
|
||||
if (first != null) {
|
||||
// if not set get any entry
|
||||
return getExtractorById(first, nextPage, localization);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getDefaultKioskId() {
|
||||
return defaultKiosk;
|
||||
}
|
||||
|
||||
public KioskExtractor getExtractorById(final String kioskId, final Page nextPage)
|
||||
throws ExtractionException, IOException {
|
||||
return getExtractorById(kioskId, nextPage, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
public KioskExtractor getExtractorById(final String kioskId,
|
||||
final Page nextPage,
|
||||
final Localization localization)
|
||||
throws ExtractionException, IOException {
|
||||
final KioskEntry ke = kioskList.get(kioskId);
|
||||
if (ke == null) {
|
||||
throw new ExtractionException("No kiosk found with the type: " + kioskId);
|
||||
} else {
|
||||
final KioskExtractor kioskExtractor = ke.extractorFactory.createNewKiosk(service,
|
||||
ke.handlerFactory.fromId(kioskId).getUrl(), kioskId);
|
||||
|
||||
if (forcedLocalization != null) {
|
||||
kioskExtractor.forceLocalization(forcedLocalization);
|
||||
}
|
||||
if (forcedContentCountry != null) {
|
||||
kioskExtractor.forceContentCountry(forcedContentCountry);
|
||||
}
|
||||
|
||||
return kioskExtractor;
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getAvailableKiosks() {
|
||||
return kioskList.keySet();
|
||||
}
|
||||
|
||||
public KioskExtractor getExtractorByUrl(final String url, final Page nextPage)
|
||||
throws ExtractionException, IOException {
|
||||
return getExtractorByUrl(url, nextPage, NewPipe.getPreferredLocalization());
|
||||
}
|
||||
|
||||
public KioskExtractor getExtractorByUrl(final String url,
|
||||
final Page nextPage,
|
||||
final Localization localization)
|
||||
throws ExtractionException, IOException {
|
||||
for (final Map.Entry<String, KioskEntry> e : kioskList.entrySet()) {
|
||||
final KioskEntry ke = e.getValue();
|
||||
if (ke.handlerFactory.acceptUrl(url)) {
|
||||
return getExtractorById(ke.handlerFactory.getId(url), nextPage, localization);
|
||||
}
|
||||
}
|
||||
throw new ExtractionException("Could not find a kiosk that fits to the url: " + url);
|
||||
}
|
||||
|
||||
public ListLinkHandlerFactory getListLinkHandlerFactoryByType(final String type) {
|
||||
return kioskList.get(type).handlerFactory;
|
||||
}
|
||||
|
||||
public void forceLocalization(@Nullable final Localization localization) {
|
||||
this.forcedLocalization = localization;
|
||||
}
|
||||
|
||||
public void forceContentCountry(@Nullable final ContentCountry contentCountry) {
|
||||
this.forcedContentCountry = contentCountry;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class LinkHandler implements Serializable {
|
||||
protected final String originalUrl;
|
||||
protected final String url;
|
||||
protected final String id;
|
||||
|
||||
public LinkHandler(final String originalUrl, final String url, final String id) {
|
||||
this.originalUrl = originalUrl;
|
||||
this.url = url;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LinkHandler(final LinkHandler handler) {
|
||||
this(handler.originalUrl, handler.url, handler.id);
|
||||
}
|
||||
|
||||
public String getOriginalUrl() {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getBaseUrl() throws ParsingException {
|
||||
return Utils.getBaseUrl(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/*
|
||||
* Created by Christian Schabesberger on 26.07.16.
|
||||
*
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* LinkHandlerFactory.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public abstract class LinkHandlerFactory {
|
||||
|
||||
///////////////////////////////////
|
||||
// To Override
|
||||
///////////////////////////////////
|
||||
|
||||
public abstract String getId(String url) throws ParsingException;
|
||||
|
||||
public abstract String getUrl(String id) throws ParsingException;
|
||||
|
||||
public abstract boolean onAcceptUrl(String url) throws ParsingException;
|
||||
|
||||
public String getUrl(final String id, final String baseUrl) throws ParsingException {
|
||||
return getUrl(id);
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// Logic
|
||||
///////////////////////////////////
|
||||
|
||||
/**
|
||||
* Builds a {@link LinkHandler} from a url.<br>
|
||||
* Be sure to call {@link Utils#followGoogleRedirectIfNeeded(String)} on the url if overriding
|
||||
* this function.
|
||||
*
|
||||
* @param url the url to extract path and id from
|
||||
* @return a {@link LinkHandler} complete with information
|
||||
*/
|
||||
public LinkHandler fromUrl(final String url) throws ParsingException {
|
||||
if (Utils.isNullOrEmpty(url)) {
|
||||
throw new IllegalArgumentException("The url is null or empty");
|
||||
}
|
||||
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||
final String baseUrl = Utils.getBaseUrl(polishedUrl);
|
||||
return fromUrl(polishedUrl, baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link LinkHandler} from an URL and a base URL. The URL is expected to be already
|
||||
* polished from Google search redirects (otherwise how could {@code baseUrl} have been
|
||||
* extracted?).<br>
|
||||
* So do not call {@link Utils#followGoogleRedirectIfNeeded(String)} on the URL if overriding
|
||||
* this function, since that should be done in {@link #fromUrl(String)}.
|
||||
*
|
||||
* @param url the URL without Google search redirects to extract id from
|
||||
* @param baseUrl the base URL
|
||||
* @return a {@link LinkHandler} complete with information
|
||||
*/
|
||||
public LinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
|
||||
Objects.requireNonNull(url, "URL cannot be null");
|
||||
if (!acceptUrl(url)) {
|
||||
throw new ParsingException("URL not accepted: " + url);
|
||||
}
|
||||
|
||||
final String id = getId(url);
|
||||
return new LinkHandler(url, getUrl(id, baseUrl), id);
|
||||
}
|
||||
|
||||
public LinkHandler fromId(final String id) throws ParsingException {
|
||||
Objects.requireNonNull(id, "ID cannot be null");
|
||||
final String url = getUrl(id);
|
||||
return new LinkHandler(url, url, id);
|
||||
}
|
||||
|
||||
public LinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
|
||||
Objects.requireNonNull(id, "ID cannot be null");
|
||||
final String url = getUrl(id, baseUrl);
|
||||
return new LinkHandler(url, url, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a VIEW_ACTION is caught this function will test if the url delivered within the calling
|
||||
* Intent was meant to be watched with this Service.
|
||||
* Return false if this service shall not allow to be called through ACTIONs.
|
||||
*/
|
||||
public boolean acceptUrl(final String url) throws ParsingException {
|
||||
return onAcceptUrl(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ListLinkHandler extends LinkHandler {
|
||||
protected final List<String> contentFilters;
|
||||
protected final String sortFilter;
|
||||
|
||||
public ListLinkHandler(final String originalUrl,
|
||||
final String url,
|
||||
final String id,
|
||||
final List<String> contentFilters,
|
||||
final String sortFilter) {
|
||||
super(originalUrl, url, id);
|
||||
this.contentFilters = Collections.unmodifiableList(contentFilters);
|
||||
this.sortFilter = sortFilter;
|
||||
}
|
||||
|
||||
public ListLinkHandler(final ListLinkHandler handler) {
|
||||
this(handler.originalUrl,
|
||||
handler.url,
|
||||
handler.id,
|
||||
handler.contentFilters,
|
||||
handler.sortFilter);
|
||||
}
|
||||
|
||||
public ListLinkHandler(final LinkHandler handler) {
|
||||
this(handler.originalUrl,
|
||||
handler.url,
|
||||
handler.id,
|
||||
Collections.emptyList(),
|
||||
"");
|
||||
}
|
||||
|
||||
public List<String> getContentFilters() {
|
||||
return contentFilters;
|
||||
}
|
||||
|
||||
public String getSortFilter() {
|
||||
return sortFilter;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class ListLinkHandlerFactory extends LinkHandlerFactory {
|
||||
|
||||
///////////////////////////////////
|
||||
// To Override
|
||||
///////////////////////////////////
|
||||
|
||||
public abstract String getUrl(String id, List<String> contentFilter, String sortFilter)
|
||||
throws ParsingException;
|
||||
|
||||
public String getUrl(final String id,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final String baseUrl) throws ParsingException {
|
||||
return getUrl(id, contentFilter, sortFilter);
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// Logic
|
||||
///////////////////////////////////
|
||||
|
||||
@Override
|
||||
public ListLinkHandler fromUrl(final String url) throws ParsingException {
|
||||
final String polishedUrl = Utils.followGoogleRedirectIfNeeded(url);
|
||||
final String baseUrl = Utils.getBaseUrl(polishedUrl);
|
||||
return fromUrl(polishedUrl, baseUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandler fromUrl(final String url, final String baseUrl) throws ParsingException {
|
||||
Objects.requireNonNull(url, "URL may not be null");
|
||||
return new ListLinkHandler(super.fromUrl(url, baseUrl));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandler fromId(final String id) throws ParsingException {
|
||||
return new ListLinkHandler(super.fromId(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandler fromId(final String id, final String baseUrl) throws ParsingException {
|
||||
return new ListLinkHandler(super.fromId(id, baseUrl));
|
||||
}
|
||||
|
||||
public ListLinkHandler fromQuery(final String id,
|
||||
final List<String> contentFilters,
|
||||
final String sortFilter) throws ParsingException {
|
||||
final String url = getUrl(id, contentFilters, sortFilter);
|
||||
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
||||
}
|
||||
|
||||
public ListLinkHandler fromQuery(final String id,
|
||||
final List<String> contentFilters,
|
||||
final String sortFilter,
|
||||
final String baseUrl) throws ParsingException {
|
||||
final String url = getUrl(id, contentFilters, sortFilter, baseUrl);
|
||||
return new ListLinkHandler(url, url, id, contentFilters, sortFilter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For making ListLinkHandlerFactory compatible with LinkHandlerFactory we need to override
|
||||
* this, however it should not be overridden by the actual implementation.
|
||||
*
|
||||
* @return the url corresponding to id without any filters applied
|
||||
*/
|
||||
public String getUrl(final String id) throws ParsingException {
|
||||
return getUrl(id, new ArrayList<>(0), "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl(final String id, final String baseUrl) throws ParsingException {
|
||||
return getUrl(id, new ArrayList<>(0), "", baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will returns content filter the corresponding extractor can handle like "channels", "videos",
|
||||
* "music", etc.
|
||||
*
|
||||
* @return filter that can be applied when building a query for getting a list
|
||||
*/
|
||||
public String[] getAvailableContentFilter() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Will returns sort filter the corresponding extractor can handle like "A-Z", "oldest first",
|
||||
* "size", etc.
|
||||
*
|
||||
* @return filter that can be applied when building a query for getting a list
|
||||
*/
|
||||
public String[] getAvailableSortFilter() {
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SearchQueryHandler extends ListLinkHandler {
|
||||
|
||||
public SearchQueryHandler(final String originalUrl,
|
||||
final String url,
|
||||
final String searchString,
|
||||
final List<String> contentFilters,
|
||||
final String sortFilter) {
|
||||
super(originalUrl, url, searchString, contentFilters, sortFilter);
|
||||
}
|
||||
|
||||
public SearchQueryHandler(final ListLinkHandler handler) {
|
||||
this(handler.originalUrl,
|
||||
handler.url,
|
||||
handler.id,
|
||||
handler.contentFilters,
|
||||
handler.sortFilter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the search string. Since ListQIHandler is based on ListLinkHandler
|
||||
* getSearchString() is equivalent to calling getId().
|
||||
*
|
||||
* @return the search string
|
||||
*/
|
||||
public String getSearchString() {
|
||||
return getId();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package org.schabi.newpipe.extractor.linkhandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class SearchQueryHandlerFactory extends ListLinkHandlerFactory {
|
||||
|
||||
///////////////////////////////////
|
||||
// To Override
|
||||
///////////////////////////////////
|
||||
|
||||
@Override
|
||||
public abstract String getUrl(String query, List<String> contentFilter, String sortFilter)
|
||||
throws ParsingException;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public String getSearchString(final String url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// Logic
|
||||
///////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getId(final String url) {
|
||||
return getSearchString(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchQueryHandler fromQuery(final String query,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) throws ParsingException {
|
||||
return new SearchQueryHandler(super.fromQuery(query, contentFilter, sortFilter));
|
||||
}
|
||||
|
||||
public SearchQueryHandler fromQuery(final String query) throws ParsingException {
|
||||
return fromQuery(query, Collections.emptyList(), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* It's not mandatory for NewPipe to handle the Url
|
||||
*/
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a country that should be used when fetching content.
|
||||
* <p>
|
||||
* YouTube, for example, give different results in their feed depending on which country is
|
||||
* selected.
|
||||
* </p>
|
||||
*/
|
||||
public class ContentCountry implements Serializable {
|
||||
public static final ContentCountry DEFAULT =
|
||||
new ContentCountry(Localization.DEFAULT.getCountryCode());
|
||||
|
||||
@Nonnull
|
||||
private final String countryCode;
|
||||
|
||||
public static List<ContentCountry> listFrom(final String... countryCodeList) {
|
||||
final List<ContentCountry> toReturn = new ArrayList<>();
|
||||
for (final String countryCode : countryCodeList) {
|
||||
toReturn.add(new ContentCountry(countryCode));
|
||||
}
|
||||
return Collections.unmodifiableList(toReturn);
|
||||
}
|
||||
|
||||
public ContentCountry(@Nonnull final String countryCode) {
|
||||
this.countryCode = countryCode;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getCountryCode() {
|
||||
return countryCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getCountryCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ContentCountry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ContentCountry that = (ContentCountry) o;
|
||||
|
||||
return countryCode.equals(that.countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return countryCode.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Serializable;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
/**
|
||||
* A wrapper class that provides a field to describe if the date/time is precise or just an
|
||||
* approximation.
|
||||
*/
|
||||
public class DateWrapper implements Serializable {
|
||||
@Nonnull
|
||||
private final OffsetDateTime offsetDateTime;
|
||||
private final boolean isApproximation;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #DateWrapper(OffsetDateTime)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public DateWrapper(@Nonnull final Calendar calendar) {
|
||||
//noinspection deprecation
|
||||
this(calendar, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #DateWrapper(OffsetDateTime, boolean)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public DateWrapper(@Nonnull final Calendar calendar, final boolean isApproximation) {
|
||||
this(OffsetDateTime.ofInstant(calendar.toInstant(), ZoneOffset.UTC), isApproximation);
|
||||
}
|
||||
|
||||
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime) {
|
||||
this(offsetDateTime, false);
|
||||
}
|
||||
|
||||
public DateWrapper(@Nonnull final OffsetDateTime offsetDateTime,
|
||||
final boolean isApproximation) {
|
||||
this.offsetDateTime = offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
|
||||
this.isApproximation = isApproximation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the wrapped date/time as a {@link Calendar}.
|
||||
* @deprecated use {@link #offsetDateTime()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
@Nonnull
|
||||
public Calendar date() {
|
||||
return GregorianCalendar.from(offsetDateTime.toZonedDateTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the wrapped date/time.
|
||||
*/
|
||||
@Nonnull
|
||||
public OffsetDateTime offsetDateTime() {
|
||||
return offsetDateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the date is considered is precise or just an approximation (e.g. service only
|
||||
* returns an approximation like 2 weeks ago instead of a precise date).
|
||||
*/
|
||||
public boolean isApproximation() {
|
||||
return isApproximation;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.utils.LocaleCompat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class Localization implements Serializable {
|
||||
public static final Localization DEFAULT = new Localization("en", "GB");
|
||||
|
||||
@Nonnull
|
||||
private final String languageCode;
|
||||
@Nullable
|
||||
private final String countryCode;
|
||||
|
||||
/**
|
||||
* @param localizationCodeList a list of localization code, formatted like {@link
|
||||
* #getLocalizationCode()}
|
||||
*/
|
||||
public static List<Localization> listFrom(final String... localizationCodeList) {
|
||||
final List<Localization> toReturn = new ArrayList<>();
|
||||
for (final String localizationCode : localizationCodeList) {
|
||||
toReturn.add(fromLocalizationCode(localizationCode));
|
||||
}
|
||||
return Collections.unmodifiableList(toReturn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param localizationCode a localization code, formatted like {@link #getLocalizationCode()}
|
||||
*/
|
||||
public static Localization fromLocalizationCode(final String localizationCode) {
|
||||
return fromLocale(LocaleCompat.forLanguageTag(localizationCode));
|
||||
}
|
||||
|
||||
public Localization(@Nonnull final String languageCode, @Nullable final String countryCode) {
|
||||
this.languageCode = languageCode;
|
||||
this.countryCode = countryCode;
|
||||
}
|
||||
|
||||
public Localization(@Nonnull final String languageCode) {
|
||||
this(languageCode, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getLanguageCode() {
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getCountryCode() {
|
||||
return countryCode == null ? "" : countryCode;
|
||||
}
|
||||
|
||||
public Locale asLocale() {
|
||||
return new Locale(getLanguageCode(), getCountryCode());
|
||||
}
|
||||
|
||||
public static Localization fromLocale(@Nonnull final Locale locale) {
|
||||
return new Localization(locale.getLanguage(), locale.getCountry());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted string in the form of: {@code language-Country}, or
|
||||
* just {@code language} if country is {@code null}.
|
||||
*/
|
||||
public String getLocalizationCode() {
|
||||
return languageCode + (countryCode == null ? "" : "-" + countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Localization[" + getLocalizationCode() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Localization)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Localization that = (Localization) o;
|
||||
|
||||
return languageCode.equals(that.languageCode)
|
||||
&& Objects.equals(countryCode, that.countryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = languageCode.hashCode();
|
||||
result = 31 * result + Objects.hashCode(countryCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a three letter language code (ISO 639-2/T) to a Locale
|
||||
* because limits of Java Locale class.
|
||||
*
|
||||
* @param code a three letter language code
|
||||
* @return the Locale corresponding
|
||||
*/
|
||||
public static Locale getLocaleFromThreeLetterCode(@Nonnull final String code)
|
||||
throws ParsingException {
|
||||
final String[] languages = Locale.getISOLanguages();
|
||||
final Map<String, Locale> localeMap = new HashMap<>(languages.length);
|
||||
for (final String language : languages) {
|
||||
final Locale locale = new Locale(language);
|
||||
localeMap.put(locale.getISO3Language(), locale);
|
||||
}
|
||||
if (localeMap.containsKey(code)) {
|
||||
return localeMap.get(code);
|
||||
} else {
|
||||
throw new ParsingException(
|
||||
"Could not get Locale from this three letter language code" + code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
|
||||
import org.schabi.newpipe.extractor.utils.Parser;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A helper class that is meant to be used by services that need to parse upload dates in the
|
||||
* format '2 days ago' or similar.
|
||||
*/
|
||||
public class TimeAgoParser {
|
||||
private final PatternsHolder patternsHolder;
|
||||
private final OffsetDateTime now;
|
||||
|
||||
/**
|
||||
* Creates a helper to parse upload dates in the format '2 days ago'.
|
||||
* <p>
|
||||
* Instantiate a new {@link TimeAgoParser} every time you extract a new batch of items.
|
||||
* </p>
|
||||
*
|
||||
* @param patternsHolder An object that holds the "time ago" patterns, special cases, and the
|
||||
* language word separator.
|
||||
*/
|
||||
public TimeAgoParser(final PatternsHolder patternsHolder) {
|
||||
this.patternsHolder = patternsHolder;
|
||||
now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a textual date in the format '2 days ago' into a Calendar representation which is then
|
||||
* wrapped in a {@link DateWrapper} object.
|
||||
* <p>
|
||||
* Beginning with days ago, the date is considered as an approximation.
|
||||
*
|
||||
* @param textualDate The original date as provided by the streaming service
|
||||
* @return The parsed time (can be approximated)
|
||||
* @throws ParsingException if the time unit could not be recognized
|
||||
*/
|
||||
public DateWrapper parse(final String textualDate) throws ParsingException {
|
||||
for (final Map.Entry<ChronoUnit, Map<String, Integer>> caseUnitEntry
|
||||
: patternsHolder.specialCases().entrySet()) {
|
||||
final ChronoUnit chronoUnit = caseUnitEntry.getKey();
|
||||
for (final Map.Entry<String, Integer> caseMapToAmountEntry
|
||||
: caseUnitEntry.getValue().entrySet()) {
|
||||
final String caseText = caseMapToAmountEntry.getKey();
|
||||
final Integer caseAmount = caseMapToAmountEntry.getValue();
|
||||
|
||||
if (textualDateMatches(textualDate, caseText)) {
|
||||
return getResultFor(caseAmount, chronoUnit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getResultFor(parseTimeAgoAmount(textualDate), parseChronoUnit(textualDate));
|
||||
}
|
||||
|
||||
private int parseTimeAgoAmount(final String textualDate) {
|
||||
try {
|
||||
return Integer.parseInt(textualDate.replaceAll("\\D+", ""));
|
||||
} catch (final NumberFormatException ignored) {
|
||||
// If there is no valid number in the textual date,
|
||||
// assume it is 1 (as in 'a second ago').
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private ChronoUnit parseChronoUnit(final String textualDate) throws ParsingException {
|
||||
return patternsHolder.asMap().entrySet().stream()
|
||||
.filter(e -> e.getValue().stream()
|
||||
.anyMatch(agoPhrase -> textualDateMatches(textualDate, agoPhrase)))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElseThrow(() ->
|
||||
new ParsingException("Unable to parse the date: " + textualDate));
|
||||
}
|
||||
|
||||
private boolean textualDateMatches(final String textualDate, final String agoPhrase) {
|
||||
if (textualDate.equals(agoPhrase)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (patternsHolder.wordSeparator().isEmpty()) {
|
||||
return textualDate.toLowerCase().contains(agoPhrase.toLowerCase());
|
||||
}
|
||||
|
||||
final String escapedPhrase = Pattern.quote(agoPhrase.toLowerCase());
|
||||
final String escapedSeparator = patternsHolder.wordSeparator().equals(" ")
|
||||
// From JDK8 → \h - Treat horizontal spaces as a normal one
|
||||
// (non-breaking space, thin space, etc.)
|
||||
? "[ \\t\\xA0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000]"
|
||||
: Pattern.quote(patternsHolder.wordSeparator());
|
||||
|
||||
// (^|separator)pattern($|separator)
|
||||
// Check if the pattern is surrounded by separators or start/end of the string.
|
||||
final String pattern =
|
||||
"(^|" + escapedSeparator + ")" + escapedPhrase + "($|" + escapedSeparator + ")";
|
||||
|
||||
return Parser.isMatch(pattern, textualDate.toLowerCase());
|
||||
}
|
||||
|
||||
private DateWrapper getResultFor(final int timeAgoAmount, final ChronoUnit chronoUnit) {
|
||||
OffsetDateTime offsetDateTime = now;
|
||||
boolean isApproximation = false;
|
||||
|
||||
switch (chronoUnit) {
|
||||
case SECONDS:
|
||||
case MINUTES:
|
||||
case HOURS:
|
||||
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
|
||||
break;
|
||||
|
||||
case DAYS:
|
||||
case WEEKS:
|
||||
case MONTHS:
|
||||
offsetDateTime = offsetDateTime.minus(timeAgoAmount, chronoUnit);
|
||||
isApproximation = true;
|
||||
break;
|
||||
|
||||
case YEARS:
|
||||
// minusDays is needed to prevent `PrettyTime` from showing '12 months ago'.
|
||||
offsetDateTime = offsetDateTime.minusYears(timeAgoAmount).minusDays(1);
|
||||
isApproximation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isApproximation) {
|
||||
offsetDateTime = offsetDateTime.truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
|
||||
return new DateWrapper(offsetDateTime, isApproximation);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.schabi.newpipe.extractor.localization;
|
||||
|
||||
import org.schabi.newpipe.extractor.timeago.PatternsHolder;
|
||||
import org.schabi.newpipe.extractor.timeago.PatternsManager;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class TimeAgoPatternsManager {
|
||||
private TimeAgoPatternsManager() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PatternsHolder getPatternsFor(@Nonnull final Localization localization) {
|
||||
return PatternsManager.getPatterns(localization.getLanguageCode(),
|
||||
localization.getCountryCode());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TimeAgoParser getTimeAgoParserFor(@Nonnull final Localization localization) {
|
||||
final PatternsHolder holder = getPatternsFor(localization);
|
||||
|
||||
if (holder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TimeAgoParser(holder);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package org.schabi.newpipe.extractor.playlist;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
||||
|
||||
public PlaylistExtractor(final StreamingService service, final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
public abstract String getUploaderUrl() throws ParsingException;
|
||||
public abstract String getUploaderName() throws ParsingException;
|
||||
public abstract String getUploaderAvatarUrl() throws ParsingException;
|
||||
public abstract boolean isUploaderVerified() throws ParsingException;
|
||||
|
||||
public abstract long getStreamCount() throws ParsingException;
|
||||
|
||||
@Nonnull
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getBannerUrl() throws ParsingException {
|
||||
// Banner can't be handled by frontend right now.
|
||||
// Whoever is willing to implement this should also implement it in the frontend.
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSubChannelName() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSubChannelUrl() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getSubChannelAvatarUrl() throws ParsingException {
|
||||
return "";
|
||||
}
|
||||
|
||||
public PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
|
||||
return PlaylistInfo.PlaylistType.NORMAL;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
package org.schabi.newpipe.extractor.playlist;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class PlaylistInfo extends ListInfo<StreamInfoItem> {
|
||||
|
||||
/**
|
||||
* Mixes are handled as particular playlists in NewPipeExtractor. {@link PlaylistType#NORMAL} is
|
||||
* for non-mixes, while other values are for the different types of mixes. The type of a mix
|
||||
* depends on how its contents are autogenerated.
|
||||
*/
|
||||
public enum PlaylistType {
|
||||
/**
|
||||
* A normal playlist (not a mix)
|
||||
*/
|
||||
NORMAL,
|
||||
|
||||
/**
|
||||
* A mix made only of streams related to a particular stream, for example YouTube mixes
|
||||
*/
|
||||
MIX_STREAM,
|
||||
|
||||
/**
|
||||
* A mix made only of music streams related to a particular stream, for example YouTube
|
||||
* music mixes
|
||||
*/
|
||||
MIX_MUSIC,
|
||||
|
||||
/**
|
||||
* A mix made only of streams from (or related to) the same channel, for example YouTube
|
||||
* channel mixes
|
||||
*/
|
||||
MIX_CHANNEL,
|
||||
|
||||
/**
|
||||
* A mix made only of streams related to a particular (musical) genre, for example YouTube
|
||||
* genre mixes
|
||||
*/
|
||||
MIX_GENRE,
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
private PlaylistInfo(final int serviceId, final ListLinkHandler linkHandler, final String name)
|
||||
throws ParsingException {
|
||||
super(serviceId, linkHandler, name);
|
||||
}
|
||||
|
||||
public static PlaylistInfo getInfo(final String url) throws IOException, ExtractionException {
|
||||
return getInfo(NewPipe.getServiceByUrl(url), url);
|
||||
}
|
||||
|
||||
public static PlaylistInfo getInfo(final StreamingService service, final String url)
|
||||
throws IOException, ExtractionException {
|
||||
final PlaylistExtractor extractor = service.getPlaylistExtractor(url);
|
||||
extractor.fetchPage();
|
||||
return getInfo(extractor);
|
||||
}
|
||||
|
||||
public static InfoItemsPage<StreamInfoItem> getMoreItems(final StreamingService service,
|
||||
final String url,
|
||||
final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return service.getPlaylistExtractor(url).getPage(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PlaylistInfo from PlaylistExtractor
|
||||
*
|
||||
* @param extractor an extractor where fetchPage() was already got called on.
|
||||
*/
|
||||
public static PlaylistInfo getInfo(final PlaylistExtractor extractor)
|
||||
throws ExtractionException {
|
||||
|
||||
final PlaylistInfo info = new PlaylistInfo(
|
||||
extractor.getServiceId(),
|
||||
extractor.getLinkHandler(),
|
||||
extractor.getName());
|
||||
// collect uploader extraction failures until we are sure this is not
|
||||
// just a playlist without an uploader
|
||||
final List<Throwable> uploaderParsingErrors = new ArrayList<>();
|
||||
|
||||
try {
|
||||
info.setOriginalUrl(extractor.getOriginalUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setStreamCount(extractor.getStreamCount());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setThumbnailUrl(extractor.getThumbnailUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setUploaderUrl(extractor.getUploaderUrl());
|
||||
} catch (final Exception e) {
|
||||
info.setUploaderUrl("");
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setUploaderName(extractor.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
info.setUploaderName("");
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setUploaderAvatarUrl(extractor.getUploaderAvatarUrl());
|
||||
} catch (final Exception e) {
|
||||
info.setUploaderAvatarUrl("");
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setSubChannelUrl(extractor.getSubChannelUrl());
|
||||
} catch (final Exception e) {
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setSubChannelName(extractor.getSubChannelName());
|
||||
} catch (final Exception e) {
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setSubChannelAvatarUrl(extractor.getSubChannelAvatarUrl());
|
||||
} catch (final Exception e) {
|
||||
uploaderParsingErrors.add(e);
|
||||
}
|
||||
try {
|
||||
info.setBannerUrl(extractor.getBannerUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setPlaylistType(extractor.getPlaylistType());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
// do not fail if everything but the uploader infos could be collected (TODO better comment)
|
||||
if (!uploaderParsingErrors.isEmpty()
|
||||
&& (!info.getErrors().isEmpty() || uploaderParsingErrors.size() < 3)) {
|
||||
info.addAllErrors(uploaderParsingErrors);
|
||||
}
|
||||
|
||||
final InfoItemsPage<StreamInfoItem> itemsPage
|
||||
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||
info.setRelatedItems(itemsPage.getItems());
|
||||
info.setNextPage(itemsPage.getNextPage());
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private String thumbnailUrl;
|
||||
private String bannerUrl;
|
||||
private String uploaderUrl;
|
||||
private String uploaderName;
|
||||
private String uploaderAvatarUrl;
|
||||
private String subChannelUrl;
|
||||
private String subChannelName;
|
||||
private String subChannelAvatarUrl;
|
||||
private long streamCount = 0;
|
||||
private PlaylistType playlistType;
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getBannerUrl() {
|
||||
return bannerUrl;
|
||||
}
|
||||
|
||||
public void setBannerUrl(final String bannerUrl) {
|
||||
this.bannerUrl = bannerUrl;
|
||||
}
|
||||
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
public void setUploaderUrl(final String uploaderUrl) {
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
}
|
||||
|
||||
public String getUploaderName() {
|
||||
return uploaderName;
|
||||
}
|
||||
|
||||
public void setUploaderName(final String uploaderName) {
|
||||
this.uploaderName = uploaderName;
|
||||
}
|
||||
|
||||
public String getUploaderAvatarUrl() {
|
||||
return uploaderAvatarUrl;
|
||||
}
|
||||
|
||||
public void setUploaderAvatarUrl(final String uploaderAvatarUrl) {
|
||||
this.uploaderAvatarUrl = uploaderAvatarUrl;
|
||||
}
|
||||
|
||||
public String getSubChannelUrl() {
|
||||
return subChannelUrl;
|
||||
}
|
||||
|
||||
public void setSubChannelUrl(final String subChannelUrl) {
|
||||
this.subChannelUrl = subChannelUrl;
|
||||
}
|
||||
|
||||
public String getSubChannelName() {
|
||||
return subChannelName;
|
||||
}
|
||||
|
||||
public void setSubChannelName(final String subChannelName) {
|
||||
this.subChannelName = subChannelName;
|
||||
}
|
||||
|
||||
public String getSubChannelAvatarUrl() {
|
||||
return subChannelAvatarUrl;
|
||||
}
|
||||
|
||||
public void setSubChannelAvatarUrl(final String subChannelAvatarUrl) {
|
||||
this.subChannelAvatarUrl = subChannelAvatarUrl;
|
||||
}
|
||||
|
||||
public long getStreamCount() {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(final long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
public PlaylistType getPlaylistType() {
|
||||
return playlistType;
|
||||
}
|
||||
|
||||
public void setPlaylistType(final PlaylistType playlistType) {
|
||||
this.playlistType = playlistType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package org.schabi.newpipe.extractor.playlist;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class PlaylistInfoItem extends InfoItem {
|
||||
|
||||
private String uploaderName;
|
||||
private String uploaderUrl;
|
||||
private boolean uploaderVerified;
|
||||
/**
|
||||
* How many streams this playlist have
|
||||
*/
|
||||
private long streamCount = 0;
|
||||
private PlaylistInfo.PlaylistType playlistType;
|
||||
|
||||
public PlaylistInfoItem(final int serviceId, final String url, final String name) {
|
||||
super(InfoType.PLAYLIST, serviceId, url, name);
|
||||
}
|
||||
|
||||
public String getUploaderName() {
|
||||
return uploaderName;
|
||||
}
|
||||
|
||||
public void setUploaderName(final String uploaderName) {
|
||||
this.uploaderName = uploaderName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
public void setUploaderUrl(@Nullable final String uploaderUrl) {
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
}
|
||||
|
||||
public boolean isUploaderVerified() {
|
||||
return uploaderVerified;
|
||||
}
|
||||
|
||||
public void setUploaderVerified(final boolean uploaderVerified) {
|
||||
this.uploaderVerified = uploaderVerified;
|
||||
}
|
||||
|
||||
public long getStreamCount() {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(final long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
public PlaylistInfo.PlaylistType getPlaylistType() {
|
||||
return playlistType;
|
||||
}
|
||||
|
||||
public void setPlaylistType(final PlaylistInfo.PlaylistType playlistType) {
|
||||
this.playlistType = playlistType;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package org.schabi.newpipe.extractor.playlist;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public interface PlaylistInfoItemExtractor extends InfoItemExtractor {
|
||||
|
||||
/**
|
||||
* Get the uploader name
|
||||
* @return the uploader name
|
||||
*/
|
||||
String getUploaderName() throws ParsingException;
|
||||
|
||||
/**
|
||||
* Get the uploader url
|
||||
* @return the uploader url
|
||||
*/
|
||||
String getUploaderUrl() throws ParsingException;
|
||||
|
||||
/**
|
||||
* Get whether the uploader is verified
|
||||
* @return whether the uploader is verified
|
||||
*/
|
||||
boolean isUploaderVerified() throws ParsingException;
|
||||
|
||||
/**
|
||||
* Get the number of streams
|
||||
* @return the number of streams
|
||||
*/
|
||||
long getStreamCount() throws ParsingException;
|
||||
|
||||
/**
|
||||
* @return the type of this playlist, see {@link PlaylistInfo.PlaylistType} for a description
|
||||
* of types. If not overridden always returns {@link PlaylistInfo.PlaylistType#NORMAL}.
|
||||
*/
|
||||
@Nonnull
|
||||
default PlaylistInfo.PlaylistType getPlaylistType() throws ParsingException {
|
||||
return PlaylistInfo.PlaylistType.NORMAL;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package org.schabi.newpipe.extractor.playlist;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
public class PlaylistInfoItemsCollector
|
||||
extends InfoItemsCollector<PlaylistInfoItem, PlaylistInfoItemExtractor> {
|
||||
|
||||
public PlaylistInfoItemsCollector(final int serviceId) {
|
||||
super(serviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaylistInfoItem extract(final PlaylistInfoItemExtractor extractor)
|
||||
throws ParsingException {
|
||||
final PlaylistInfoItem resultItem = new PlaylistInfoItem(
|
||||
getServiceId(), extractor.getUrl(), extractor.getName());
|
||||
|
||||
try {
|
||||
resultItem.setUploaderName(extractor.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploaderUrl(extractor.getUploaderUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setUploaderVerified(extractor.isUploaderVerified());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setThumbnailUrl(extractor.getThumbnailUrl());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setStreamCount(extractor.getStreamCount());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
try {
|
||||
resultItem.setPlaylistType(extractor.getPlaylistType());
|
||||
} catch (final Exception e) {
|
||||
addError(e);
|
||||
}
|
||||
return resultItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package org.schabi.newpipe.extractor.search;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class SearchExtractor extends ListExtractor<InfoItem> {
|
||||
|
||||
public static class NothingFoundException extends ExtractionException {
|
||||
public NothingFoundException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchExtractor(final StreamingService service, final SearchQueryHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
public String getSearchString() {
|
||||
return getLinkHandler().getSearchString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The search suggestion provided by the service.
|
||||
* <p>
|
||||
* This method also returns the corrected query if
|
||||
* {@link SearchExtractor#isCorrectedSearch()} is true.
|
||||
*
|
||||
* @return a suggestion to another query, the corrected query, or an empty String.
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract String getSearchSuggestion() throws ParsingException;
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public SearchQueryHandler getLinkHandler() {
|
||||
return (SearchQueryHandler) super.getLinkHandler();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() {
|
||||
return getLinkHandler().getSearchString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell if the search was corrected by the service (if it's not exactly the search you typed).
|
||||
* <p>
|
||||
* Example: on YouTube, if you search for "pewdeipie",
|
||||
* it will give you results for "pewdiepie", then isCorrectedSearch should return true.
|
||||
*
|
||||
* @return whether the results comes from a corrected query or not.
|
||||
*/
|
||||
public abstract boolean isCorrectedSearch() throws ParsingException;
|
||||
|
||||
/**
|
||||
* Meta information about the search query.
|
||||
* <p>
|
||||
* Example: on YouTube, if you search for "Covid-19",
|
||||
* there is a box with information from the WHO about Covid-19 and a link to the WHO's website.
|
||||
* @return additional meta information about the search query
|
||||
*/
|
||||
@Nonnull
|
||||
public abstract List<MetaInfo> getMetaInfo() throws ParsingException;
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package org.schabi.newpipe.extractor.search;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class SearchInfo extends ListInfo<InfoItem> {
|
||||
private final String searchString;
|
||||
private String searchSuggestion;
|
||||
private boolean isCorrectedSearch;
|
||||
private List<MetaInfo> metaInfo;
|
||||
|
||||
public SearchInfo(final int serviceId,
|
||||
final SearchQueryHandler qIHandler,
|
||||
final String searchString) {
|
||||
super(serviceId, qIHandler, "Search");
|
||||
this.searchString = searchString;
|
||||
}
|
||||
|
||||
|
||||
public static SearchInfo getInfo(final StreamingService service,
|
||||
final SearchQueryHandler searchQuery)
|
||||
throws ExtractionException, IOException {
|
||||
final SearchExtractor extractor = service.getSearchExtractor(searchQuery);
|
||||
extractor.fetchPage();
|
||||
return getInfo(extractor);
|
||||
}
|
||||
|
||||
public static SearchInfo getInfo(final SearchExtractor extractor)
|
||||
throws ExtractionException, IOException {
|
||||
final SearchInfo info = new SearchInfo(
|
||||
extractor.getServiceId(),
|
||||
extractor.getLinkHandler(),
|
||||
extractor.getSearchString());
|
||||
|
||||
try {
|
||||
info.setOriginalUrl(extractor.getOriginalUrl());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setSearchSuggestion(extractor.getSearchSuggestion());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setIsCorrectedSearch(extractor.isCorrectedSearch());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
try {
|
||||
info.setMetaInfo(extractor.getMetaInfo());
|
||||
} catch (final Exception e) {
|
||||
info.addError(e);
|
||||
}
|
||||
|
||||
final ListExtractor.InfoItemsPage<InfoItem> page
|
||||
= ExtractorHelper.getItemsPageOrLogError(info, extractor);
|
||||
info.setRelatedItems(page.getItems());
|
||||
info.setNextPage(page.getNextPage());
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
public static ListExtractor.InfoItemsPage<InfoItem> getMoreItems(final StreamingService service,
|
||||
final SearchQueryHandler query,
|
||||
final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return service.getSearchExtractor(query).getPage(page);
|
||||
}
|
||||
|
||||
// Getter
|
||||
public String getSearchString() {
|
||||
return this.searchString;
|
||||
}
|
||||
|
||||
public String getSearchSuggestion() {
|
||||
return this.searchSuggestion;
|
||||
}
|
||||
|
||||
public boolean isCorrectedSearch() {
|
||||
return this.isCorrectedSearch;
|
||||
}
|
||||
|
||||
public void setIsCorrectedSearch(final boolean isCorrectedSearch) {
|
||||
this.isCorrectedSearch = isCorrectedSearch;
|
||||
}
|
||||
|
||||
public void setSearchSuggestion(final String searchSuggestion) {
|
||||
this.searchSuggestion = searchSuggestion;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<MetaInfo> getMetaInfo() {
|
||||
return metaInfo;
|
||||
}
|
||||
|
||||
public void setMetaInfo(@Nonnull final List<MetaInfo> metaInfo) {
|
||||
this.metaInfo = metaInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.FEATURED_API_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.KIOSK_FEATURED;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.KIOSK_RADIO;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.RADIO_API_URL;
|
||||
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampCommentsExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampPlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSearchExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampSuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampChannelLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampCommentsLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampFeaturedLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampPlaylistLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.linkHandler.BandcampStreamLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class BandcampService extends StreamingService {
|
||||
|
||||
public BandcampService(final int id) {
|
||||
super(id, "Bandcamp", Arrays.asList(AUDIO, COMMENTS));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return BASE_URL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkHandlerFactory getStreamLHFactory() {
|
||||
return new BandcampStreamLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getChannelLHFactory() {
|
||||
return new BandcampChannelLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getPlaylistLHFactory() {
|
||||
return new BandcampPlaylistLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchQueryHandlerFactory getSearchQHFactory() {
|
||||
return new BandcampSearchQueryHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getCommentsLHFactory() {
|
||||
return new BandcampCommentsLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchExtractor getSearchExtractor(final SearchQueryHandler queryHandler) {
|
||||
return new BandcampSearchExtractor(this, queryHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionExtractor getSuggestionExtractor() {
|
||||
return new BandcampSuggestionExtractor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionExtractor getSubscriptionExtractor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskList getKioskList() throws ExtractionException {
|
||||
|
||||
final KioskList kioskList = new KioskList(this);
|
||||
|
||||
try {
|
||||
kioskList.addKioskEntry(
|
||||
(streamingService, url, kioskId) -> new BandcampFeaturedExtractor(
|
||||
BandcampService.this,
|
||||
new BandcampFeaturedLinkHandlerFactory().fromUrl(FEATURED_API_URL),
|
||||
kioskId
|
||||
),
|
||||
new BandcampFeaturedLinkHandlerFactory(),
|
||||
KIOSK_FEATURED
|
||||
);
|
||||
|
||||
kioskList.addKioskEntry(
|
||||
(streamingService, url, kioskId) -> new BandcampRadioExtractor(
|
||||
BandcampService.this,
|
||||
new BandcampFeaturedLinkHandlerFactory().fromUrl(RADIO_API_URL),
|
||||
kioskId
|
||||
),
|
||||
new BandcampFeaturedLinkHandlerFactory(),
|
||||
KIOSK_RADIO
|
||||
);
|
||||
|
||||
kioskList.setDefaultKiosk(KIOSK_FEATURED);
|
||||
|
||||
} catch (final Exception e) {
|
||||
throw new ExtractionException(e);
|
||||
}
|
||||
|
||||
return kioskList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
|
||||
return new BandcampChannelExtractor(this, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
|
||||
return new BandcampPlaylistExtractor(this, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamExtractor getStreamExtractor(final LinkHandler linkHandler) {
|
||||
if (BandcampExtractorHelper.isRadioUrl(linkHandler.getUrl())) {
|
||||
return new BandcampRadioStreamExtractor(this, linkHandler);
|
||||
}
|
||||
return new BandcampStreamExtractor(this, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentsExtractor getCommentsExtractor(final ListLinkHandler linkHandler) {
|
||||
return new BandcampCommentsExtractor(this, linkHandler);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.replaceHttpWithHttps;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelExtractor;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampDiscographStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class BandcampChannelExtractor extends ChannelExtractor {
|
||||
|
||||
private JsonObject channelInfo;
|
||||
|
||||
public BandcampChannelExtractor(final StreamingService service,
|
||||
final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarUrl() {
|
||||
if (channelInfo.getLong("bio_image_id") == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return BandcampExtractorHelper.getImageUrl(channelInfo.getLong("bio_image_id"), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBannerUrl() throws ParsingException {
|
||||
/*
|
||||
* Mobile API does not return the header or not the correct header.
|
||||
* Therefore, we need to query the website
|
||||
*/
|
||||
try {
|
||||
final String html = getDownloader()
|
||||
.get(replaceHttpWithHttps(channelInfo.getString("bandcamp_url")))
|
||||
.responseBody();
|
||||
|
||||
return Stream.of(Jsoup.parse(html).getElementById("customHeader"))
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(element -> element.getElementsByTag("img").stream())
|
||||
.map(element -> element.attr("src"))
|
||||
.findFirst()
|
||||
.orElse(""); // no banner available
|
||||
|
||||
} catch (final IOException | ReCaptchaException e) {
|
||||
throw new ParsingException("Could not download artist web site", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bandcamp discontinued their RSS feeds because it hadn't been used enough.
|
||||
*/
|
||||
@Override
|
||||
public String getFeedUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSubscriberCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return channelInfo.getString("bio");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ParsingException {
|
||||
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
|
||||
final JsonArray discography = channelInfo.getArray("discography");
|
||||
|
||||
for (int i = 0; i < discography.size(); i++) {
|
||||
// A discograph is as an item appears in a discography
|
||||
final JsonObject discograph = discography.getObject(i);
|
||||
|
||||
if (!discograph.getString("item_type").equals("track")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collector.commit(new BandcampDiscographStreamInfoItemExtractor(discograph, getUrl()));
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
channelInfo = BandcampExtractorHelper.getArtistDetails(getId());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() {
|
||||
return channelInfo.getString("name");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
|
||||
public class BandcampChannelInfoItemExtractor implements ChannelInfoItemExtractor {
|
||||
|
||||
private final Element resultInfo;
|
||||
private final Element searchResult;
|
||||
|
||||
public BandcampChannelInfoItemExtractor(final Element searchResult) {
|
||||
this.searchResult = searchResult;
|
||||
resultInfo = searchResult.getElementsByClass("result-info").first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return resultInfo.getElementsByClass("heading").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return resultInfo.getElementsByClass("itemurl").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return resultInfo.getElementsByClass("subhead").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSubscriberCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsExtractor;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
||||
public class BandcampCommentsExtractor extends CommentsExtractor {
|
||||
|
||||
private Document document;
|
||||
|
||||
|
||||
public BandcampCommentsExtractor(final StreamingService service,
|
||||
final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
document = Jsoup.parse(downloader.get(getLinkHandler().getUrl()).responseBody());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getInitialPage()
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
final CommentsInfoItemsCollector collector = new CommentsInfoItemsCollector(getServiceId());
|
||||
|
||||
final Elements writings = document.getElementsByClass("writing");
|
||||
|
||||
for (final Element writing : writings) {
|
||||
collector.commit(new BandcampCommentsInfoItemExtractor(writing, getUrl()));
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<CommentsInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class BandcampCommentsInfoItemExtractor implements CommentsInfoItemExtractor {
|
||||
|
||||
private final Element writing;
|
||||
private final String url;
|
||||
|
||||
public BandcampCommentsInfoItemExtractor(final Element writing, final String url) {
|
||||
this.writing = writing;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return getCommentText().getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return writing.getElementsByClass("thumb").attr("src");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Description getCommentText() throws ParsingException {
|
||||
final var text = writing.getElementsByClass("text").stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Element::ownText)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ParsingException("Could not get comment text"));
|
||||
|
||||
return new Description(text, Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return writing.getElementsByClass("name").stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(Element::text)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return writing.getElementsByClass("thumb").attr("src");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class BandcampExtractorHelper {
|
||||
|
||||
public static final String BASE_URL = "https://bandcamp.com";
|
||||
public static final String BASE_API_URL = BASE_URL + "/api";
|
||||
|
||||
private BandcampExtractorHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate all these parameters together to the URL of the corresponding album or track
|
||||
* using the mobile API
|
||||
*/
|
||||
public static String getStreamUrlFromIds(final long bandId,
|
||||
final long itemId,
|
||||
final String itemType) throws ParsingException {
|
||||
try {
|
||||
final String jsonString = NewPipe.getDownloader().get(
|
||||
BASE_API_URL + "/mobile/22/tralbum_details?band_id=" + bandId
|
||||
+ "&tralbum_id=" + itemId + "&tralbum_type=" + itemType.charAt(0))
|
||||
.responseBody();
|
||||
|
||||
return Utils.replaceHttpWithHttps(JsonParser.object().from(jsonString)
|
||||
.getString("bandcamp_url"));
|
||||
|
||||
} catch (final JsonParserException | ReCaptchaException | IOException e) {
|
||||
throw new ParsingException("Ids could not be translated to URL", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch artist details from mobile endpoint.
|
||||
* <a href="https://notabug.org/fynngodau/bandcampDirect/wiki/
|
||||
* rewindBandcamp+%E2%80%93+Fetching+artist+details">
|
||||
* More technical info.</a>
|
||||
*/
|
||||
public static JsonObject getArtistDetails(final String id) throws ParsingException {
|
||||
try {
|
||||
return JsonParser.object().from(NewPipe.getDownloader().postWithContentTypeJson(
|
||||
BASE_API_URL + "/mobile/22/band_details",
|
||||
Collections.emptyMap(),
|
||||
JsonWriter.string()
|
||||
.object()
|
||||
.value("band_id", id)
|
||||
.end()
|
||||
.done()
|
||||
.getBytes(StandardCharsets.UTF_8)).responseBody());
|
||||
} catch (final IOException | ReCaptchaException | JsonParserException e) {
|
||||
throw new ParsingException("Could not download band details", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate image url from image ID.
|
||||
* <p>
|
||||
* The appendix "_10" was chosen because it provides images sized 1200x1200. Other integer
|
||||
* values are possible as well (e.g. 0 is a very large resolution, possibly the original).
|
||||
*
|
||||
* @param id The image ID
|
||||
* @param album True if this is the cover of an album or track
|
||||
* @return URL of image with this ID sized 1200x1200
|
||||
*/
|
||||
public static String getImageUrl(final long id, final boolean album) {
|
||||
return "https://f4.bcbits.com/img/" + (album ? 'a' : "") + id + "_10.jpg";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the given URL looks like it comes from a bandcamp custom domain
|
||||
* or if it comes from <code>bandcamp.com</code> itself
|
||||
*/
|
||||
public static boolean isSupportedDomain(final String url) throws ParsingException {
|
||||
|
||||
// Accept all bandcamp.com URLs
|
||||
if (url.toLowerCase().matches("https?://.+\\.bandcamp\\.com(/.*)?")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test other URLs for whether they contain a footer that links to bandcamp
|
||||
return Jsoup.parse(NewPipe.getDownloader().get(url).responseBody())
|
||||
.getElementById("pgFt")
|
||||
.getElementById("pgFt-inner")
|
||||
.getElementById("footer-logo-wrapper")
|
||||
.getElementById("footer-logo")
|
||||
.getElementsByClass("hiddenAccess")
|
||||
.text().equals("Bandcamp");
|
||||
} catch (final NullPointerException e) {
|
||||
return false;
|
||||
} catch (final IOException | ReCaptchaException e) {
|
||||
throw new ParsingException("Could not determine whether URL is custom domain "
|
||||
+ "(not available? network error?)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the URL points to a radio kiosk.
|
||||
* @param url the URL to check
|
||||
* @return true if the URL matches {@code https://bandcamp.com/?show=SHOW_ID}
|
||||
*/
|
||||
public static boolean isRadioUrl(final String url) {
|
||||
return url.toLowerCase().matches("https?://bandcamp\\.com/\\?show=\\d+");
|
||||
}
|
||||
|
||||
public static DateWrapper parseDate(final String textDate) throws ParsingException {
|
||||
try {
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.parse(textDate,
|
||||
DateTimeFormatter.ofPattern("dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH));
|
||||
return new DateWrapper(zonedDateTime.toOffsetDateTime(), false);
|
||||
} catch (final DateTimeException e) {
|
||||
throw new ParsingException("Could not parse date '" + textDate + "'", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getThumbnailUrlFromSearchResult(final Element searchResult) {
|
||||
return searchResult.getElementsByClass("art").stream()
|
||||
.flatMap(element -> element.getElementsByTag("img").stream())
|
||||
.map(element -> element.attr("src"))
|
||||
.filter(string -> !string.isEmpty())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
|
||||
public class BandcampFeaturedExtractor extends KioskExtractor<PlaylistInfoItem> {
|
||||
|
||||
public static final String KIOSK_FEATURED = "Featured";
|
||||
public static final String FEATURED_API_URL = BASE_API_URL + "/mobile/24/bootstrap_data";
|
||||
public static final String MORE_FEATURED_API_URL
|
||||
= BASE_API_URL + "/mobile/24/feed_older_logged_out";
|
||||
|
||||
private JsonObject json;
|
||||
|
||||
public BandcampFeaturedExtractor(final StreamingService streamingService,
|
||||
final ListLinkHandler listLinkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, listLinkHandler, kioskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
try {
|
||||
json = JsonParser.object().from(getDownloader().postWithContentTypeJson(
|
||||
FEATURED_API_URL,
|
||||
Collections.emptyMap(),
|
||||
"{\"platform\":\"\",\"version\":0}".getBytes(StandardCharsets.UTF_8))
|
||||
.responseBody());
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Could not parse Bandcamp featured API response", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return KIOSK_FEATURED;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<PlaylistInfoItem> getInitialPage()
|
||||
throws IOException, ExtractionException {
|
||||
final JsonArray featuredStories = json.getObject("feed_content")
|
||||
.getObject("stories")
|
||||
.getArray("featured");
|
||||
|
||||
return extractItems(featuredStories);
|
||||
}
|
||||
|
||||
private InfoItemsPage<PlaylistInfoItem> extractItems(final JsonArray featuredStories) {
|
||||
final PlaylistInfoItemsCollector c = new PlaylistInfoItemsCollector(getServiceId());
|
||||
|
||||
for (int i = 0; i < featuredStories.size(); i++) {
|
||||
final JsonObject featuredStory = featuredStories.getObject(i);
|
||||
|
||||
if (featuredStory.isNull("album_title")) {
|
||||
// Is not an album, ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
c.commit(new BandcampPlaylistInfoItemFeaturedExtractor(featuredStory));
|
||||
}
|
||||
|
||||
final JsonObject lastFeaturedStory = featuredStories.getObject(featuredStories.size() - 1);
|
||||
return new InfoItemsPage<>(c, getNextPageFrom(lastFeaturedStory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Next Page can be generated from metadata of last featured story
|
||||
*/
|
||||
private Page getNextPageFrom(final JsonObject lastFeaturedStory) {
|
||||
final long lastStoryDate = lastFeaturedStory.getLong("story_date");
|
||||
final long lastStoryId = lastFeaturedStory.getLong("ntid");
|
||||
final String lastStoryType = lastFeaturedStory.getString("story_type");
|
||||
return new Page(
|
||||
MORE_FEATURED_API_URL + "?story_groups=featured"
|
||||
+ ':' + lastStoryDate + ':' + lastStoryType + ':' + lastStoryId
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<PlaylistInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
|
||||
final JsonObject response;
|
||||
try {
|
||||
response = JsonParser.object().from(
|
||||
getDownloader().get(page.getUrl()).responseBody()
|
||||
);
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Could not parse Bandcamp featured API response", e);
|
||||
}
|
||||
|
||||
return extractItems(response.getObject("stories").getArray("featured"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampStreamExtractor.getAlbumInfoJson;
|
||||
import static org.schabi.newpipe.extractor.utils.JsonUtils.getJsonData;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampPlaylistStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class BandcampPlaylistExtractor extends PlaylistExtractor {
|
||||
|
||||
/**
|
||||
* An arbitrarily chosen number above which cover arts won't be fetched individually for each
|
||||
* track; instead, it will be assumed that every track has the same cover art as the album,
|
||||
* which is not always the case.
|
||||
*/
|
||||
private static final int MAXIMUM_INDIVIDUAL_COVER_ARTS = 10;
|
||||
|
||||
private Document document;
|
||||
private JsonObject albumJson;
|
||||
private JsonArray trackInfo;
|
||||
private String name;
|
||||
|
||||
public BandcampPlaylistExtractor(final StreamingService service,
|
||||
final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
|
||||
document = Jsoup.parse(html);
|
||||
albumJson = getAlbumInfoJson(html);
|
||||
trackInfo = albumJson.getArray("trackinfo");
|
||||
|
||||
try {
|
||||
name = getJsonData(html, "data-embed").getString("album_title");
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
|
||||
} catch (final ArrayIndexOutOfBoundsException e) {
|
||||
throw new ParsingException("JSON does not exist", e);
|
||||
}
|
||||
|
||||
if (trackInfo.isEmpty()) {
|
||||
// Albums without trackInfo need to be purchased before they can be played
|
||||
throw new ContentNotAvailableException("Album needs to be purchased");
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
if (albumJson.isNull("art_id")) {
|
||||
return "";
|
||||
} else {
|
||||
return getImageUrl(albumJson.getLong("art_id"), true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
final String[] parts = getUrl().split("/");
|
||||
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
||||
return HTTPS + parts[2] + "/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
return albumJson.getString("artist");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return document.getElementsByClass("band-photo").stream()
|
||||
.map(element -> element.attr("src"))
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() {
|
||||
return trackInfo.size();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws ExtractionException {
|
||||
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
|
||||
for (int i = 0; i < trackInfo.size(); i++) {
|
||||
final JsonObject track = trackInfo.getObject(i);
|
||||
|
||||
if (trackInfo.size() < MAXIMUM_INDIVIDUAL_COVER_ARTS) {
|
||||
// Load cover art of every track individually
|
||||
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
|
||||
track, getUploaderUrl(), getService()));
|
||||
} else {
|
||||
// Pretend every track has the same cover art as the album
|
||||
collector.commit(new BandcampPlaylistStreamInfoItemExtractor(
|
||||
track, getUploaderUrl(), getThumbnailUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class BandcampPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||
private final Element searchResult;
|
||||
private final Element resultInfo;
|
||||
|
||||
public BandcampPlaylistInfoItemExtractor(@Nonnull final Element searchResult) {
|
||||
this.searchResult = searchResult;
|
||||
resultInfo = searchResult.getElementsByClass("result-info").first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
return resultInfo.getElementsByClass("subhead").text()
|
||||
.split(" by")[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() {
|
||||
final String length = resultInfo.getElementsByClass("length").text();
|
||||
return Integer.parseInt(length.split(" track")[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return resultInfo.getElementsByClass("heading").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return resultInfo.getElementsByClass("itemurl").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
|
||||
public class BandcampPlaylistInfoItemFeaturedExtractor implements PlaylistInfoItemExtractor {
|
||||
|
||||
private final JsonObject featuredStory;
|
||||
|
||||
public BandcampPlaylistInfoItemFeaturedExtractor(final JsonObject featuredStory) {
|
||||
this.featuredStory = featuredStory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
return featuredStory.getString("band_name");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() {
|
||||
return featuredStory.getInt("num_streamable_tracks");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return featuredStory.getString("album_title");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return featuredStory.getString("item_url").replaceAll("http://", "https://");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return featuredStory.has("art_id") ? getImageUrl(featuredStory.getLong("art_id"), true)
|
||||
: getImageUrl(featuredStory.getLong("item_art_id"), true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
|
||||
public class BandcampRadioExtractor extends KioskExtractor<StreamInfoItem> {
|
||||
|
||||
public static final String KIOSK_RADIO = "Radio";
|
||||
public static final String RADIO_API_URL = BASE_API_URL + "/bcweekly/1/list";
|
||||
|
||||
private JsonObject json = null;
|
||||
|
||||
public BandcampRadioExtractor(final StreamingService streamingService,
|
||||
final ListLinkHandler linkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, linkHandler, kioskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
try {
|
||||
json = JsonParser.object().from(
|
||||
getDownloader().get(RADIO_API_URL).responseBody());
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ExtractionException("Could not parse Bandcamp Radio API response", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return KIOSK_RADIO;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
|
||||
final JsonArray radioShows = json.getArray("results");
|
||||
|
||||
for (int i = 0; i < radioShows.size(); i++) {
|
||||
final JsonObject radioShow = radioShows.getObject(i);
|
||||
collector.commit(new BandcampRadioInfoItemExtractor(radioShow));
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
|
||||
public class BandcampRadioInfoItemExtractor implements StreamInfoItemExtractor {
|
||||
|
||||
private final JsonObject show;
|
||||
|
||||
public BandcampRadioInfoItemExtractor(final JsonObject radioShow) {
|
||||
show = radioShow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
/* Duration is only present in the more detailed information that has to be queried
|
||||
separately. Therefore, over 300 queries would be needed every time the kiosk is opened if we
|
||||
were to display the real value. */
|
||||
//return query(show.getInt("id")).getLong("audio_duration");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() {
|
||||
return show.getString("date");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DateWrapper getUploadDate() throws ParsingException {
|
||||
return BandcampExtractorHelper.parseDate(getTextualUploadDate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return show.getString("subtitle");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return BASE_URL + "/?show=" + show.getInt("id");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() {
|
||||
return getImageUrl(show.getLong("image_id"), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamType getStreamType() {
|
||||
return StreamType.AUDIO_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
// JSON does not contain uploader name
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAd() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamSegment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BandcampRadioStreamExtractor extends BandcampStreamExtractor {
|
||||
|
||||
private static final String OPUS_LO = "opus-lo";
|
||||
private static final String MP3_128 = "mp3-128";
|
||||
private JsonObject showInfo;
|
||||
|
||||
public BandcampRadioStreamExtractor(final StreamingService service,
|
||||
final LinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
static JsonObject query(final int id) throws ParsingException {
|
||||
try {
|
||||
return JsonParser.object().from(NewPipe.getDownloader()
|
||||
.get(BASE_API_URL + "/bcweekly/1/get?id=" + id).responseBody());
|
||||
} catch (final IOException | ReCaptchaException | JsonParserException e) {
|
||||
throw new ParsingException("could not get show data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
showInfo = query(Integer.parseInt(getId()));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
/* Select "subtitle" and not "audio_title", as the latter would cause a lot of
|
||||
* items to show the same title, e.g. "Bandcamp Weekly".
|
||||
*/
|
||||
return showInfo.getString("subtitle");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderUrl() throws ContentNotSupportedException {
|
||||
throw new ContentNotSupportedException("Fan pages are not supported");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return getLinkHandler().getUrl();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return Jsoup.parse(showInfo.getString("image_caption")).getElementsByTag("a").stream()
|
||||
.map(Element::text)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new ParsingException("Could not get uploader name"));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() {
|
||||
return showInfo.getString("published_date");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return getImageUrl(showInfo.getLong("show_image_id"), false);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return BASE_URL + "/img/buttons/bandcamp-button-circle-whitecolor-512.png";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Description getDescription() {
|
||||
return new Description(showInfo.getString("desc"), Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength() {
|
||||
return showInfo.getLong("audio_duration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioStream> getAudioStreams() {
|
||||
final List<AudioStream> audioStreams = new ArrayList<>();
|
||||
final JsonObject streams = showInfo.getObject("audio_stream");
|
||||
|
||||
if (streams.has(MP3_128)) {
|
||||
audioStreams.add(new AudioStream.Builder()
|
||||
.setId(MP3_128)
|
||||
.setContent(streams.getString(MP3_128), true)
|
||||
.setMediaFormat(MediaFormat.MP3)
|
||||
.setAverageBitrate(128)
|
||||
.build());
|
||||
}
|
||||
|
||||
if (streams.has(OPUS_LO)) {
|
||||
audioStreams.add(new AudioStream.Builder()
|
||||
.setId(OPUS_LO)
|
||||
.setContent(streams.getString(OPUS_LO), true)
|
||||
.setMediaFormat(MediaFormat.OPUS)
|
||||
.setAverageBitrate(100).build());
|
||||
}
|
||||
|
||||
return audioStreams;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<StreamSegment> getStreamSegments() throws ParsingException {
|
||||
final JsonArray tracks = showInfo.getArray("tracks");
|
||||
final List<StreamSegment> segments = new ArrayList<>(tracks.size());
|
||||
for (final Object t : tracks) {
|
||||
final JsonObject track = (JsonObject) t;
|
||||
final StreamSegment segment = new StreamSegment(
|
||||
track.getString("title"), track.getInt("timecode"));
|
||||
// "track art" is the track's album cover
|
||||
segment.setPreviewUrl(getImageUrl(track.getLong("track_art_id"), true));
|
||||
segment.setChannelName(track.getString("artist"));
|
||||
segments.add(segment);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getLicence() {
|
||||
// Contrary to other Bandcamp streams, radio streams don't have a license
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getCategory() {
|
||||
// Contrary to other Bandcamp streams, radio streams don't have categories
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
// Contrary to other Bandcamp streams, radio streams don't have tags
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaylistInfoItemsCollector getRelatedItems() {
|
||||
// Contrary to other Bandcamp streams, radio streams don't have related items
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Created by Fynn Godau 2021, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemExtractor;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Extracts recommended albums from tracks' website
|
||||
*/
|
||||
public class BandcampRelatedPlaylistInfoItemExtractor implements PlaylistInfoItemExtractor {
|
||||
private final Element relatedAlbum;
|
||||
|
||||
public BandcampRelatedPlaylistInfoItemExtractor(@Nonnull final Element relatedAlbum) {
|
||||
this.relatedAlbum = relatedAlbum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return relatedAlbum.getElementsByClass("release-title").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return relatedAlbum.getElementsByClass("title-and-artist").attr("abs:href");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return relatedAlbum.getElementsByClass("album-art").attr("src");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return relatedAlbum.getElementsByClass("by-artist").text().replace("by ", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getStreamCount() throws ParsingException {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.MetaInfo;
|
||||
import org.schabi.newpipe.extractor.MultiInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem.BandcampSearchStreamInfoItemExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class BandcampSearchExtractor extends SearchExtractor {
|
||||
|
||||
public BandcampSearchExtractor(final StreamingService service,
|
||||
final SearchQueryHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getSearchSuggestion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCorrectedSearch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<MetaInfo> getMetaInfo() throws ParsingException {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public InfoItemsPage<InfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
final MultiInfoItemsCollector collector = new MultiInfoItemsCollector(getServiceId());
|
||||
final Document d = Jsoup.parse(getDownloader().get(page.getUrl()).responseBody());
|
||||
|
||||
for (final Element searchResult : d.getElementsByClass("searchresult")) {
|
||||
final String type = searchResult.getElementsByClass("result-info").stream()
|
||||
.flatMap(element -> element.getElementsByClass("itemtype").stream())
|
||||
.map(Element::text)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
|
||||
switch (type) {
|
||||
case "ARTIST":
|
||||
collector.commit(new BandcampChannelInfoItemExtractor(searchResult));
|
||||
break;
|
||||
case "ALBUM":
|
||||
collector.commit(new BandcampPlaylistInfoItemExtractor(searchResult));
|
||||
break;
|
||||
case "TRACK":
|
||||
collector.commit(new BandcampSearchStreamInfoItemExtractor(searchResult, null));
|
||||
break;
|
||||
default:
|
||||
// don't display fan results ("FAN") or other things
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Count pages
|
||||
final Elements pageLists = d.getElementsByClass("pagelist");
|
||||
if (pageLists.isEmpty()) {
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
final Elements pages = pageLists.stream()
|
||||
.map(element -> element.getElementsByTag("li"))
|
||||
.findFirst()
|
||||
.orElseGet(Elements::new);
|
||||
|
||||
// Find current page
|
||||
int currentPage = -1;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
final Element pageElement = pages.get(i);
|
||||
if (!pageElement.getElementsByTag("span").isEmpty()) {
|
||||
currentPage = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search results appear to be capped at six pages
|
||||
assert pages.size() < 10;
|
||||
|
||||
String nextUrl = null;
|
||||
if (currentPage < pages.size()) {
|
||||
nextUrl = page.getUrl().substring(0, page.getUrl().length() - 1) + (currentPage + 1);
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, new Page(nextUrl));
|
||||
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<InfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||
return getPage(new Page(getUrl()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.getImageUrl;
|
||||
import static org.schabi.newpipe.extractor.utils.Utils.HTTPS;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItemsCollector;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BandcampStreamExtractor extends StreamExtractor {
|
||||
private JsonObject albumJson;
|
||||
private JsonObject current;
|
||||
private Document document;
|
||||
|
||||
public BandcampStreamExtractor(final StreamingService service, final LinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String html = downloader.get(getLinkHandler().getUrl()).responseBody();
|
||||
document = Jsoup.parse(html);
|
||||
albumJson = getAlbumInfoJson(html);
|
||||
current = albumJson.getObject("current");
|
||||
|
||||
if (albumJson.getArray("trackinfo").size() > 1) {
|
||||
// In this case, we are actually viewing an album page!
|
||||
throw new ExtractionException("Page is actually an album, not a track");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON that contains album's metadata from page
|
||||
*
|
||||
* @param html Website
|
||||
* @return Album metadata JSON
|
||||
* @throws ParsingException In case of a faulty website
|
||||
*/
|
||||
public static JsonObject getAlbumInfoJson(final String html) throws ParsingException {
|
||||
try {
|
||||
return JsonUtils.getJsonData(html, "data-tralbum");
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ParsingException("Faulty JSON; page likely does not contain album data", e);
|
||||
} catch (final ArrayIndexOutOfBoundsException e) {
|
||||
throw new ParsingException("JSON does not exist", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return current.getString("title");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
final String[] parts = getUrl().split("/");
|
||||
// https: (/) (/) * .bandcamp.com (/) and leave out the rest
|
||||
return HTTPS + parts[2] + "/";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return albumJson.getString("url").replace("http://", "https://");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return albumJson.getString("artist");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() {
|
||||
return current.getString("publish_date");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DateWrapper getUploadDate() throws ParsingException {
|
||||
return BandcampExtractorHelper.parseDate(getTextualUploadDate());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
if (albumJson.isNull("art_id")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return getImageUrl(albumJson.getLong("art_id"), true);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return document.getElementsByClass("band-photo").stream()
|
||||
.map(element -> element.attr("src"))
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Description getDescription() {
|
||||
final String s = Utils.nonEmptyAndNullJoin("\n\n", current.getString("about"),
|
||||
current.getString("lyrics"), current.getString("credits"));
|
||||
return new Description(s, Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioStream> getAudioStreams() {
|
||||
return Collections.singletonList(new AudioStream.Builder()
|
||||
.setId("mp3-128")
|
||||
.setContent(albumJson.getArray("trackinfo")
|
||||
.getObject(0)
|
||||
.getObject("file")
|
||||
.getString("mp3-128"), true)
|
||||
.setMediaFormat(MediaFormat.MP3)
|
||||
.setAverageBitrate(128)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLength() throws ParsingException {
|
||||
return (long) albumJson.getArray("trackinfo").getObject(0)
|
||||
.getDouble("duration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoStreams() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoOnlyStreams() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamType getStreamType() {
|
||||
return StreamType.AUDIO_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaylistInfoItemsCollector getRelatedItems() {
|
||||
final PlaylistInfoItemsCollector collector = new PlaylistInfoItemsCollector(getServiceId());
|
||||
document.getElementsByClass("recommended-album")
|
||||
.stream()
|
||||
.map(BandcampRelatedPlaylistInfoItemExtractor::new)
|
||||
.forEach(collector::commit);
|
||||
|
||||
return collector;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getCategory() {
|
||||
// Get first tag from html, which is the artist's Genre
|
||||
return document.getElementsByClass("tralbum-tags").stream()
|
||||
.flatMap(element -> element.getElementsByClass("tag").stream())
|
||||
.map(Element::text)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getLicence() {
|
||||
/*
|
||||
Tests resulted in this mapping of ints to licence:
|
||||
https://cloud.disroot.org/s/ZTWBxbQ9fKRmRWJ/preview (screenshot from a Bandcamp artist's
|
||||
account)
|
||||
*/
|
||||
|
||||
switch (current.getInt("license_type")) {
|
||||
case 1:
|
||||
return "All rights reserved ©";
|
||||
case 2:
|
||||
return "CC BY-NC-ND 3.0";
|
||||
case 3:
|
||||
return "CC BY-NC-SA 3.0";
|
||||
case 4:
|
||||
return "CC BY-NC 3.0";
|
||||
case 5:
|
||||
return "CC BY-ND 3.0";
|
||||
case 6:
|
||||
return "CC BY 3.0";
|
||||
case 8:
|
||||
return "CC BY-SA 3.0";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
return document.getElementsByAttributeValue("itemprop", "keywords")
|
||||
.stream()
|
||||
.map(Element::text)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_API_URL;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BandcampSuggestionExtractor extends SuggestionExtractor {
|
||||
|
||||
private static final String AUTOCOMPLETE_URL = BASE_API_URL + "/fuzzysearch/1/autocomplete?q=";
|
||||
public BandcampSuggestionExtractor(final StreamingService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggestionList(final String query) throws IOException, ExtractionException {
|
||||
final Downloader downloader = NewPipe.getDownloader();
|
||||
|
||||
try {
|
||||
final JsonObject fuzzyResults = JsonParser.object().from(downloader
|
||||
.get(AUTOCOMPLETE_URL + Utils.encodeUrlUtf8(query)).responseBody());
|
||||
|
||||
return fuzzyResults.getObject("auto").getArray("results").stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(jsonObject -> jsonObject.getString("name"))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
} catch (final JsonParserException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BandcampDiscographStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
|
||||
|
||||
private final JsonObject discograph;
|
||||
public BandcampDiscographStreamInfoItemExtractor(final JsonObject discograph,
|
||||
final String uploaderUrl) {
|
||||
super(uploaderUrl);
|
||||
this.discograph = discograph;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
return discograph.getString("band_name");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return discograph.getString("title");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return BandcampExtractorHelper.getStreamUrlFromIds(
|
||||
discograph.getLong("band_id"),
|
||||
discograph.getLong("item_id"),
|
||||
discograph.getString("item_type")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return BandcampExtractorHelper.getImageUrl(
|
||||
discograph.getLong("art_id"), true
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class BandcampPlaylistStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
|
||||
|
||||
private final JsonObject track;
|
||||
private String substituteCoverUrl;
|
||||
private final StreamingService service;
|
||||
|
||||
public BandcampPlaylistStreamInfoItemExtractor(final JsonObject track,
|
||||
final String uploaderUrl,
|
||||
final StreamingService service) {
|
||||
super(uploaderUrl);
|
||||
this.track = track;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
public BandcampPlaylistStreamInfoItemExtractor(final JsonObject track,
|
||||
final String uploaderUrl,
|
||||
final String substituteCoverUrl) {
|
||||
this(track, uploaderUrl, (StreamingService) null);
|
||||
this.substituteCoverUrl = substituteCoverUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return track.getString("title");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return getUploaderUrl() + track.getString("title_link");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return track.getLong("duration");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
/* Tracks can have an individual artist name, but it is not included in the
|
||||
* given JSON.
|
||||
*/
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each track can have its own cover art. Therefore, unless a substitute is provided,
|
||||
* the thumbnail is extracted using a stream extractor.
|
||||
*/
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
if (substituteCoverUrl != null) {
|
||||
return substituteCoverUrl;
|
||||
} else {
|
||||
try {
|
||||
final StreamExtractor extractor = service.getStreamExtractor(getUrl());
|
||||
extractor.fetchPage();
|
||||
return extractor.getThumbnailUrl();
|
||||
} catch (final ExtractionException | IOException e) {
|
||||
throw new ParsingException("could not download cover art location", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BandcampSearchStreamInfoItemExtractor extends BandcampStreamInfoItemExtractor {
|
||||
|
||||
private final Element resultInfo;
|
||||
private final Element searchResult;
|
||||
|
||||
public BandcampSearchStreamInfoItemExtractor(final Element searchResult,
|
||||
final String uploaderUrl) {
|
||||
super(uploaderUrl);
|
||||
this.searchResult = searchResult;
|
||||
resultInfo = searchResult.getElementsByClass("result-info").first();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() {
|
||||
final String subhead = resultInfo.getElementsByClass("subhead").text();
|
||||
final String[] splitBy = subhead.split("by ");
|
||||
if (splitBy.length > 1) {
|
||||
return splitBy[1];
|
||||
} else {
|
||||
return splitBy[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return resultInfo.getElementsByClass("heading").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return resultInfo.getElementsByClass("itemurl").text();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return BandcampExtractorHelper.getThumbnailUrlFromSearchResult(searchResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.extractors.streaminfoitem;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Implements methods that return a constant value in subclasses for better readability.
|
||||
*/
|
||||
public abstract class BandcampStreamInfoItemExtractor implements StreamInfoItemExtractor {
|
||||
private final String uploaderUrl;
|
||||
|
||||
public BandcampStreamInfoItemExtractor(final String uploaderUrl) {
|
||||
this.uploaderUrl = uploaderUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamType getStreamType() {
|
||||
return StreamType.AUDIO_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() {
|
||||
return uploaderUrl;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DateWrapper getUploadDate() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAd() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Artist do have IDs that are useful
|
||||
*/
|
||||
public class BandcampChannelLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||
|
||||
|
||||
@Override
|
||||
public String getId(final String url) throws ParsingException {
|
||||
try {
|
||||
final String response = NewPipe.getDownloader().get(url).responseBody();
|
||||
|
||||
// Use band data embedded in website to extract ID
|
||||
final JsonObject bandData = JsonUtils.getJsonData(response, "data-band");
|
||||
|
||||
return String.valueOf(bandData.getLong("id"));
|
||||
|
||||
} catch (final IOException | ReCaptchaException | ArrayIndexOutOfBoundsException
|
||||
| JsonParserException e) {
|
||||
throw new ParsingException("Download failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the mobile endpoint as a "translator" from id to url
|
||||
*/
|
||||
@Override
|
||||
public String getUrl(final String id, final List<String> contentFilter, final String sortFilter)
|
||||
throws ParsingException {
|
||||
try {
|
||||
return BandcampExtractorHelper.getArtistDetails(id)
|
||||
.getString("bandcamp_url")
|
||||
.replace("http://", "https://");
|
||||
} catch (final NullPointerException e) {
|
||||
throw new ParsingException(
|
||||
"JSON does not contain URL (invalid id?) or is otherwise invalid", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts only pages that lead to the root of an artist profile. Supports external pages.
|
||||
*/
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) throws ParsingException {
|
||||
|
||||
final String lowercaseUrl = url.toLowerCase();
|
||||
|
||||
// https: | | artist.bandcamp.com | releases
|
||||
// 0 1 2 3
|
||||
final String[] splitUrl = lowercaseUrl.split("/");
|
||||
|
||||
// URL is too short
|
||||
if (splitUrl.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have "releases" or "music" as segment after url or none at all
|
||||
if (splitUrl.length > 3 && !(
|
||||
splitUrl[3].equals("releases") || splitUrl[3].equals("music")
|
||||
)) {
|
||||
|
||||
return false;
|
||||
|
||||
} else {
|
||||
if (splitUrl[2].equals("daily.bandcamp.com")) {
|
||||
// Refuse links to daily.bandcamp.com as that is not an artist
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test whether domain is supported
|
||||
return BandcampExtractorHelper.isSupportedDomain(lowercaseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Like in {@link BandcampStreamLinkHandlerFactory}, tracks have no meaningful IDs except for
|
||||
* their URLs
|
||||
*/
|
||||
public class BandcampCommentsLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||
|
||||
@Override
|
||||
public String getId(final String url) throws ParsingException {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) throws ParsingException {
|
||||
// Don't accept URLs that don't point to a track
|
||||
if (!url.toLowerCase().matches("https?://.+\\..+/(track|album)/.+")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test whether domain is supported
|
||||
return BandcampExtractorHelper.isSupportedDomain(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl(final String id,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) throws ParsingException {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.FEATURED_API_URL;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampFeaturedExtractor.KIOSK_FEATURED;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.KIOSK_RADIO;
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampRadioExtractor.RADIO_API_URL;
|
||||
|
||||
public class BandcampFeaturedLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||
|
||||
@Override
|
||||
public String getUrl(final String id,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) {
|
||||
if (id.equals(KIOSK_FEATURED)) {
|
||||
return FEATURED_API_URL; // doesn't have a website
|
||||
} else if (id.equals(KIOSK_RADIO)) {
|
||||
return RADIO_API_URL; // doesn't have its own website
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(final String url) {
|
||||
final String fixedUrl = Utils.replaceHttpWithHttps(url);
|
||||
if (BandcampExtractorHelper.isRadioUrl(fixedUrl) || fixedUrl.equals(RADIO_API_URL)) {
|
||||
return KIOSK_RADIO;
|
||||
} else if (fixedUrl.equals(FEATURED_API_URL)) {
|
||||
return KIOSK_FEATURED;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) {
|
||||
final String fixedUrl = Utils.replaceHttpWithHttps(url);
|
||||
return fixedUrl.equals(FEATURED_API_URL)
|
||||
|| fixedUrl.equals(RADIO_API_URL)
|
||||
|| BandcampExtractorHelper.isRadioUrl(fixedUrl);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Just as with streams, the album ids are essentially useless for us.
|
||||
*/
|
||||
public class BandcampPlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||
@Override
|
||||
public String getId(final String url) throws ParsingException {
|
||||
return getUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl(final String url,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) throws ParsingException {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts all bandcamp URLs that contain /album/ behind their domain name.
|
||||
*/
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) throws ParsingException {
|
||||
|
||||
// Exclude URLs which do not lead to an album
|
||||
if (!url.toLowerCase().matches("https?://.+\\..+/album/.+")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test whether domain is supported
|
||||
return BandcampExtractorHelper.isSupportedDomain(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Created by Fynn Godau 2019, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.extractor.services.bandcamp.linkHandler;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.utils.Utils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
|
||||
public class BandcampSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
|
||||
@Override
|
||||
public String getUrl(final String query,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) throws ParsingException {
|
||||
try {
|
||||
return BASE_URL + "/search?q=" + Utils.encodeUrlUtf8(query) + "&page=1";
|
||||
} catch (final UnsupportedEncodingException e) {
|
||||
throw new ParsingException("query \"" + query + "\" could not be encoded", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
// 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.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper;
|
||||
|
||||
import static org.schabi.newpipe.extractor.services.bandcamp.extractors.BandcampExtractorHelper.BASE_URL;
|
||||
|
||||
/**
|
||||
* <p>Tracks don't have standalone ids, they are always in combination with the band id.
|
||||
* That's why id = url.</p>
|
||||
*
|
||||
* <p>Radio (bandcamp weekly) shows do have ids.</p>
|
||||
*/
|
||||
public class BandcampStreamLinkHandlerFactory extends LinkHandlerFactory {
|
||||
|
||||
|
||||
/**
|
||||
* @see BandcampStreamLinkHandlerFactory
|
||||
*/
|
||||
@Override
|
||||
public String getId(final String url) throws ParsingException {
|
||||
if (BandcampExtractorHelper.isRadioUrl(url)) {
|
||||
return url.split("bandcamp.com/\\?show=")[1];
|
||||
} else {
|
||||
return getUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up url
|
||||
* @see BandcampStreamLinkHandlerFactory
|
||||
*/
|
||||
@Override
|
||||
public String getUrl(final String input) {
|
||||
if (input.matches("\\d+")) {
|
||||
return BASE_URL + "/?show=" + input;
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts URLs that point to a bandcamp radio show or that are a bandcamp
|
||||
* domain and point to a track.
|
||||
*/
|
||||
@Override
|
||||
public boolean onAcceptUrl(final String url) throws ParsingException {
|
||||
|
||||
// Accept Bandcamp radio
|
||||
if (BandcampExtractorHelper.isRadioUrl(url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't accept URLs that don't point to a track
|
||||
if (!url.toLowerCase().matches("https?://.+\\..+/track/.+")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Test whether domain is supported
|
||||
return BandcampExtractorHelper.isSupportedDomain(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
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.media_ccc.extractors.MediaCCCConferenceExtractor;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCConferenceKiosk;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamKiosk;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCParsingHelper;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCRecentKiosk;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCSearchExtractor;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferencesListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCLiveListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCRecentListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCStreamLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.extractor.suggestion.SuggestionExtractor;
|
||||
|
||||
public class MediaCCCService extends StreamingService {
|
||||
public MediaCCCService(final int id) {
|
||||
super(id, "media.ccc.de", asList(AUDIO, VIDEO));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchExtractor getSearchExtractor(final SearchQueryHandler query) {
|
||||
return new MediaCCCSearchExtractor(this, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinkHandlerFactory getStreamLHFactory() {
|
||||
return new MediaCCCStreamLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getChannelLHFactory() {
|
||||
return new MediaCCCConferenceLinkHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getPlaylistLHFactory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchQueryHandlerFactory getSearchQHFactory() {
|
||||
return new MediaCCCSearchQueryHandlerFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamExtractor getStreamExtractor(final LinkHandler linkHandler) {
|
||||
if (MediaCCCParsingHelper.isLiveStreamId(linkHandler.getId())) {
|
||||
return new MediaCCCLiveStreamExtractor(this, linkHandler);
|
||||
}
|
||||
return new MediaCCCStreamExtractor(this, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChannelExtractor getChannelExtractor(final ListLinkHandler linkHandler) {
|
||||
return new MediaCCCConferenceExtractor(this, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaylistExtractor getPlaylistExtractor(final ListLinkHandler linkHandler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionExtractor getSuggestionExtractor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KioskList getKioskList() throws ExtractionException {
|
||||
final KioskList list = new KioskList(this);
|
||||
|
||||
// add kiosks here e.g.:
|
||||
try {
|
||||
list.addKioskEntry(
|
||||
(streamingService, url, kioskId) -> new MediaCCCConferenceKiosk(
|
||||
MediaCCCService.this,
|
||||
new MediaCCCConferencesListLinkHandlerFactory().fromUrl(url),
|
||||
kioskId
|
||||
),
|
||||
new MediaCCCConferencesListLinkHandlerFactory(),
|
||||
"conferences"
|
||||
);
|
||||
|
||||
list.addKioskEntry(
|
||||
(streamingService, url, kioskId) -> new MediaCCCRecentKiosk(
|
||||
MediaCCCService.this,
|
||||
new MediaCCCRecentListLinkHandlerFactory().fromUrl(url),
|
||||
kioskId
|
||||
),
|
||||
new MediaCCCRecentListLinkHandlerFactory(),
|
||||
"recent"
|
||||
);
|
||||
|
||||
list.addKioskEntry(
|
||||
(streamingService, url, kioskId) -> new MediaCCCLiveStreamKiosk(
|
||||
MediaCCCService.this,
|
||||
new MediaCCCLiveListLinkHandlerFactory().fromUrl(url),
|
||||
kioskId
|
||||
),
|
||||
new MediaCCCLiveListLinkHandlerFactory(),
|
||||
"live"
|
||||
);
|
||||
|
||||
list.setDefaultKiosk("recent");
|
||||
} catch (final Exception e) {
|
||||
throw new ExtractionException(e);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubscriptionExtractor getSubscriptionExtractor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListLinkHandlerFactory getCommentsLHFactory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommentsExtractor getCommentsExtractor(final ListLinkHandler linkHandler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return "https://media.ccc.de";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.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.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.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.extractors.infoItems.MediaCCCStreamInfoItemExtractor;
|
||||
import org.schabi.newpipe.extractor.services.media_ccc.linkHandler.MediaCCCConferenceLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
|
||||
public class MediaCCCConferenceExtractor extends ChannelExtractor {
|
||||
private JsonObject conferenceData;
|
||||
|
||||
public MediaCCCConferenceExtractor(final StreamingService service,
|
||||
final ListLinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAvatarUrl() {
|
||||
return conferenceData.getString("logo_url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBannerUrl() {
|
||||
return conferenceData.getString("logo_url");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSubscriberCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelName() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParentChannelAvatarUrl() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVerified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
final JsonArray events = conferenceData.getArray("events");
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
collector.commit(new MediaCCCStreamInfoItemExtractor(events.getObject(i)));
|
||||
}
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page) {
|
||||
return InfoItemsPage.emptyPage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String conferenceUrl
|
||||
= MediaCCCConferenceLinkHandlerFactory.CONFERENCE_API_ENDPOINT + getId();
|
||||
try {
|
||||
conferenceData = JsonParser.object().from(downloader.get(conferenceUrl).responseBody());
|
||||
} catch (final JsonParserException jpe) {
|
||||
throw new ExtractionException("Could not parse json returnd by url: " + conferenceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return conferenceData.getString("title");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.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.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemsCollector;
|
||||
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.services.media_ccc.extractors.infoItems.MediaCCCConferenceInfoItemExtractor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class MediaCCCConferenceKiosk extends KioskExtractor<ChannelInfoItem> {
|
||||
private JsonObject doc;
|
||||
|
||||
public MediaCCCConferenceKiosk(final StreamingService streamingService,
|
||||
final ListLinkHandler linkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, linkHandler, kioskId);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<ChannelInfoItem> getInitialPage() {
|
||||
final JsonArray conferences = doc.getArray("conferences");
|
||||
final ChannelInfoItemsCollector collector = new ChannelInfoItemsCollector(getServiceId());
|
||||
for (int i = 0; i < conferences.size(); i++) {
|
||||
collector.commit(new MediaCCCConferenceInfoItemExtractor(conferences.getObject(i)));
|
||||
}
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public InfoItemsPage<ChannelInfoItem> getPage(final Page page) {
|
||||
return InfoItemsPage.emptyPage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String site = downloader.get(getLinkHandler().getUrl(), getExtractorLocalization())
|
||||
.responseBody();
|
||||
try {
|
||||
doc = JsonParser.object().from(site);
|
||||
} catch (final JsonParserException jpe) {
|
||||
throw new ExtractionException("Could not parse json.", jpe);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return doc.getString("Conferences");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
||||
|
||||
import static org.schabi.newpipe.extractor.stream.AudioStream.UNKNOWN_BITRATE;
|
||||
import static org.schabi.newpipe.extractor.stream.Stream.ID_UNKNOWN;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
|
||||
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.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.DeliveryMethod;
|
||||
import org.schabi.newpipe.extractor.stream.Description;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class MediaCCCLiveStreamExtractor extends StreamExtractor {
|
||||
private static final String STREAMS = "streams";
|
||||
private static final String URLS = "urls";
|
||||
private static final String URL = "url";
|
||||
|
||||
private JsonObject conference = null;
|
||||
private String group = "";
|
||||
private JsonObject room = null;
|
||||
|
||||
public MediaCCCLiveStreamExtractor(final StreamingService service,
|
||||
final LinkHandler linkHandler) {
|
||||
super(service, linkHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final JsonArray doc = MediaCCCParsingHelper.getLiveStreams(downloader,
|
||||
getExtractorLocalization());
|
||||
// Find the correct room
|
||||
for (int c = 0; c < doc.size(); c++) {
|
||||
final JsonObject conferenceObject = doc.getObject(c);
|
||||
final JsonArray groups = conferenceObject.getArray("groups");
|
||||
for (int g = 0; g < groups.size(); g++) {
|
||||
final String groupObject = groups.getObject(g).getString("group");
|
||||
final JsonArray rooms = groups.getObject(g).getArray("rooms");
|
||||
for (int r = 0; r < rooms.size(); r++) {
|
||||
final JsonObject roomObject = rooms.getObject(r);
|
||||
if (getId().equals(conferenceObject.getString("slug") + "/"
|
||||
+ roomObject.getString("slug"))) {
|
||||
conference = conferenceObject;
|
||||
group = groupObject;
|
||||
room = roomObject;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ExtractionException("Could not find room matching id: '" + getId() + "'");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return room.getString("display");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return room.getString("thumb");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Description getDescription() throws ParsingException {
|
||||
return new Description(conference.getString("description")
|
||||
+ " - " + group, Description.PLAIN_TEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
return "https://streaming.media.ccc.de/" + conference.getString("slug");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return conference.getString("conference");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the first DASH stream found.
|
||||
*
|
||||
* <p>
|
||||
* There can be several DASH streams, so the URL of the first one found is returned by this
|
||||
* method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* You can find the other DASH video streams by using {@link #getVideoStreams()}
|
||||
* </p>
|
||||
*/
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getDashMpdUrl() throws ParsingException {
|
||||
return getManifestOfDeliveryMethodWanted("dash");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the first HLS stream found.
|
||||
*
|
||||
* <p>
|
||||
* There can be several HLS streams, so the URL of the first one found is returned by this
|
||||
* method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* You can find the other HLS video streams by using {@link #getVideoStreams()}
|
||||
* </p>
|
||||
*/
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getHlsUrl() {
|
||||
return getManifestOfDeliveryMethodWanted("hls");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String getManifestOfDeliveryMethodWanted(@Nonnull final String deliveryMethod) {
|
||||
return room.getArray(STREAMS).stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(streamObject -> streamObject.getObject(URLS))
|
||||
.filter(urls -> urls.has(deliveryMethod))
|
||||
.map(urls -> urls.getObject(deliveryMethod).getString(URL, ""))
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioStream> getAudioStreams() throws IOException, ExtractionException {
|
||||
return getStreams("audio",
|
||||
dto -> {
|
||||
final AudioStream.Builder builder = new AudioStream.Builder()
|
||||
.setId(dto.urlValue.getString("tech", ID_UNKNOWN))
|
||||
.setContent(dto.urlValue.getString(URL), true)
|
||||
.setAverageBitrate(UNKNOWN_BITRATE);
|
||||
|
||||
if ("hls".equals(dto.urlKey)) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
return builder.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build();
|
||||
}
|
||||
|
||||
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey))
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoStreams() throws IOException, ExtractionException {
|
||||
return getStreams("video",
|
||||
dto -> {
|
||||
final JsonArray videoSize = dto.streamJsonObj.getArray("videoSize");
|
||||
|
||||
final VideoStream.Builder builder = new VideoStream.Builder()
|
||||
.setId(dto.urlValue.getString("tech", ID_UNKNOWN))
|
||||
.setContent(dto.urlValue.getString(URL), true)
|
||||
.setIsVideoOnly(false)
|
||||
.setResolution(videoSize.getInt(0) + "x" + videoSize.getInt(1));
|
||||
|
||||
if ("hls".equals(dto.urlKey)) {
|
||||
// We don't know with the type string what media format will
|
||||
// have HLS streams.
|
||||
// However, the tech string may contain some information
|
||||
// about the media format used.
|
||||
return builder.setDeliveryMethod(DeliveryMethod.HLS)
|
||||
.build();
|
||||
}
|
||||
|
||||
return builder.setMediaFormat(MediaFormat.getFromSuffix(dto.urlKey))
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is just an internal class used in {@link #getStreams(String, Function)} to tie together
|
||||
* the stream json object, its URL key and its URL value. An object of this class would be
|
||||
* temporary and the three values it holds would be <b>convert</b>ed to a proper {@link Stream}
|
||||
* object based on the wanted stream type.
|
||||
*/
|
||||
private static final class MediaCCCLiveStreamMapperDTO {
|
||||
final JsonObject streamJsonObj;
|
||||
final String urlKey;
|
||||
final JsonObject urlValue;
|
||||
|
||||
MediaCCCLiveStreamMapperDTO(final JsonObject streamJsonObj,
|
||||
final String urlKey,
|
||||
final JsonObject urlValue) {
|
||||
this.streamJsonObj = streamJsonObj;
|
||||
this.urlKey = urlKey;
|
||||
this.urlValue = urlValue;
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Stream> List<T> getStreams(
|
||||
@Nonnull final String streamType,
|
||||
@Nonnull final Function<MediaCCCLiveStreamMapperDTO, T> converter) {
|
||||
return room.getArray(STREAMS).stream()
|
||||
// Ensure that we use only process JsonObjects
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
// Only process streams of requested type
|
||||
.filter(streamJsonObj -> streamType.equals(streamJsonObj.getString("type")))
|
||||
// Flatmap Urls and ensure that we use only process JsonObjects
|
||||
.flatMap(streamJsonObj -> streamJsonObj.getObject(URLS).entrySet().stream()
|
||||
.filter(e -> e.getValue() instanceof JsonObject)
|
||||
.map(e -> new MediaCCCLiveStreamMapperDTO(
|
||||
streamJsonObj,
|
||||
e.getKey(),
|
||||
(JsonObject) e.getValue())))
|
||||
// The DASH manifest will be extracted with getDashMpdUrl
|
||||
.filter(dto -> !"dash".equals(dto.urlKey))
|
||||
// Convert
|
||||
.map(converter)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VideoStream> getVideoOnlyStreams() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamType getStreamType() throws ParsingException {
|
||||
return StreamType.LIVE_STREAM;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
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;
|
||||
|
||||
public class MediaCCCLiveStreamKiosk extends KioskExtractor<StreamInfoItem> {
|
||||
private JsonArray doc;
|
||||
|
||||
public MediaCCCLiveStreamKiosk(final StreamingService streamingService,
|
||||
final ListLinkHandler linkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, linkHandler, kioskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
doc = MediaCCCParsingHelper.getLiveStreams(downloader, getExtractorLocalization());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||
for (int c = 0; c < doc.size(); c++) {
|
||||
final JsonObject conference = doc.getObject(c);
|
||||
final JsonArray groups = conference.getArray("groups");
|
||||
for (int g = 0; g < groups.size(); g++) {
|
||||
final String group = groups.getObject(g).getString("group");
|
||||
final JsonArray rooms = groups.getObject(g).getArray("rooms");
|
||||
for (int r = 0; r < rooms.size(); r++) {
|
||||
final JsonObject room = rooms.getObject(r);
|
||||
collector.commit(new MediaCCCLiveStreamKioskExtractor(conference, group, room));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return InfoItemsPage.emptyPage();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return "live";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.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;
|
||||
|
||||
public class MediaCCCLiveStreamKioskExtractor implements StreamInfoItemExtractor {
|
||||
|
||||
private final JsonObject conferenceInfo;
|
||||
private final String group;
|
||||
private final JsonObject roomInfo;
|
||||
|
||||
public MediaCCCLiveStreamKioskExtractor(final JsonObject conferenceInfo,
|
||||
final String group,
|
||||
final JsonObject roomInfo) {
|
||||
this.conferenceInfo = conferenceInfo;
|
||||
this.group = group;
|
||||
this.roomInfo = roomInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return roomInfo.getObject("talks").getObject("current").getString("title");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() throws ParsingException {
|
||||
return roomInfo.getString("link");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getThumbnailUrl() throws ParsingException {
|
||||
return roomInfo.getString("thumb");
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamType getStreamType() throws ParsingException {
|
||||
boolean isVideo = false;
|
||||
for (final Object stream : roomInfo.getArray("streams")) {
|
||||
if ("video".equals(((JsonObject) stream).getString("type"))) {
|
||||
isVideo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isVideo ? StreamType.LIVE_STREAM : StreamType.AUDIO_LIVE_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAd() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDuration() throws ParsingException {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getViewCount() throws ParsingException {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderName() throws ParsingException {
|
||||
return conferenceInfo.getString("conference") + " - " + group
|
||||
+ " - " + roomInfo.getString("display");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUploaderUrl() throws ParsingException {
|
||||
return "https://media.ccc.de/c/" + conferenceInfo.getString("slug");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getUploaderAvatarUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUploaderVerified() throws ParsingException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getTextualUploadDate() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DateWrapper getUploadDate() throws ParsingException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.extractors;
|
||||
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
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.localization.Localization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class MediaCCCParsingHelper {
|
||||
// {conference_slug}/{room_slug}
|
||||
private static final Pattern LIVE_STREAM_ID_PATTERN = Pattern.compile("\\w+/\\w+");
|
||||
private static JsonArray liveStreams = null;
|
||||
|
||||
private MediaCCCParsingHelper() { }
|
||||
|
||||
public static OffsetDateTime parseDateFrom(final String textualUploadDate)
|
||||
throws ParsingException {
|
||||
try {
|
||||
return OffsetDateTime.parse(textualUploadDate);
|
||||
} catch (final DateTimeParseException e) {
|
||||
throw new ParsingException("Could not parse date: \"" + textualUploadDate + "\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an id is a live stream id
|
||||
* @param id the {@code id} to check
|
||||
* @return returns {@code true} if the {@code id} is formatted like
|
||||
* {@code {conference_slug}/{room_slug}}; {@code false} otherwise
|
||||
*/
|
||||
public static boolean isLiveStreamId(final String id) {
|
||||
return LIVE_STREAM_ID_PATTERN.matcher(id).find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently available live streams from
|
||||
* <a href="https://streaming.media.ccc.de/streams/v2.json">
|
||||
* https://streaming.media.ccc.de/streams/v2.json</a>.
|
||||
* Use this method to cache requests, because they can get quite big.
|
||||
* TODO: implement better caching policy (max-age: 3 min)
|
||||
* @param downloader The downloader to use for making the request
|
||||
* @param localization The localization to be used. Will most likely be ignored.
|
||||
* @return {@link JsonArray} containing current conferences and info about their rooms and
|
||||
* streams.
|
||||
* @throws ExtractionException if the data could not be fetched or the retrieved data could not
|
||||
* be parsed to a {@link JsonArray}
|
||||
*/
|
||||
public static JsonArray getLiveStreams(final Downloader downloader,
|
||||
final Localization localization)
|
||||
throws ExtractionException {
|
||||
if (liveStreams == null) {
|
||||
try {
|
||||
final String site = downloader.get("https://streaming.media.ccc.de/streams/v2.json",
|
||||
localization).responseBody();
|
||||
liveStreams = JsonParser.array().from(site);
|
||||
} catch (final IOException | ReCaptchaException e) {
|
||||
throw new ExtractionException("Could not get live stream JSON.", e);
|
||||
} catch (final JsonParserException e) {
|
||||
throw new ExtractionException("Could not parse JSON.", e);
|
||||
}
|
||||
}
|
||||
return liveStreams;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package org.schabi.newpipe.extractor.services.media_ccc.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.localization.DateWrapper;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Comparator;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class MediaCCCRecentKiosk extends KioskExtractor<StreamInfoItem> {
|
||||
|
||||
private JsonObject doc;
|
||||
|
||||
public MediaCCCRecentKiosk(final StreamingService streamingService,
|
||||
final ListLinkHandler linkHandler,
|
||||
final String kioskId) {
|
||||
super(streamingService, linkHandler, kioskId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchPage(@Nonnull final Downloader downloader)
|
||||
throws IOException, ExtractionException {
|
||||
final String site = downloader.get("https://api.media.ccc.de/public/events/recent",
|
||||
getExtractorLocalization()).responseBody();
|
||||
try {
|
||||
doc = JsonParser.object().from(site);
|
||||
} catch (final JsonParserException jpe) {
|
||||
throw new ExtractionException("Could not parse json.", jpe);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||
final JsonArray events = doc.getArray("events");
|
||||
|
||||
// Streams in the recent kiosk are not ordered by the release date.
|
||||
// Sort them to have the latest stream at the beginning of the list.
|
||||
final Comparator<StreamInfoItem> comparator = Comparator
|
||||
.comparing(StreamInfoItem::getUploadDate, Comparator
|
||||
.nullsLast(Comparator.comparing(DateWrapper::offsetDateTime)))
|
||||
.reversed();
|
||||
final StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId(),
|
||||
comparator);
|
||||
|
||||
events.stream()
|
||||
.filter(JsonObject.class::isInstance)
|
||||
.map(JsonObject.class::cast)
|
||||
.map(MediaCCCRecentKioskExtractor::new)
|
||||
// #813 / voc/voctoweb#609 -> returns faulty data -> filter it out
|
||||
.filter(extractor -> extractor.getDuration() > 0)
|
||||
.forEach(collector::commit);
|
||||
|
||||
return new InfoItemsPage<>(collector, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfoItemsPage<StreamInfoItem> getPage(final Page page)
|
||||
throws IOException, ExtractionException {
|
||||
return InfoItemsPage.emptyPage();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getName() throws ParsingException {
|
||||
return "recent";
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue