Merge pull request #307 from B0pol/playlists_peertube
[PeerTube] playlist support
This commit is contained in:
commit
b40ccb5075
|
@ -6,6 +6,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
||||||
|
|
||||||
public PlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
public PlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
||||||
|
@ -20,4 +22,9 @@ public abstract class PlaylistExtractor extends ListExtractor<StreamInfoItem> {
|
||||||
public abstract String getUploaderAvatarUrl() throws ParsingException;
|
public abstract String getUploaderAvatarUrl() throws ParsingException;
|
||||||
|
|
||||||
public abstract long getStreamCount() throws ParsingException;
|
public abstract long getStreamCount() throws ParsingException;
|
||||||
|
|
||||||
|
@Nonnull public abstract String getSubChannelName() throws ParsingException;
|
||||||
|
@Nonnull public abstract String getSubChannelUrl() throws ParsingException;
|
||||||
|
@Nonnull public abstract String getSubChannelAvatarUrl() throws ParsingException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.grack.nanojson.JsonObject;
|
||||||
import org.jsoup.helper.StringUtil;
|
import org.jsoup.helper.StringUtil;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Parser;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
@ -13,6 +14,11 @@ import java.util.TimeZone;
|
||||||
|
|
||||||
public class PeertubeParsingHelper {
|
public class PeertubeParsingHelper {
|
||||||
|
|
||||||
|
public static final String START_KEY = "start";
|
||||||
|
public static final String COUNT_KEY = "count";
|
||||||
|
public static final int ITEMS_PER_PAGE = 12;
|
||||||
|
public static final String START_PATTERN = "start=(\\d*)";
|
||||||
|
|
||||||
private PeertubeParsingHelper() {
|
private PeertubeParsingHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,4 +44,26 @@ public class PeertubeParsingHelper {
|
||||||
return uploadDate;
|
return uploadDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getNextPageUrl(String prevPageUrl, long total) {
|
||||||
|
String prevStart;
|
||||||
|
try {
|
||||||
|
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
||||||
|
} catch (Parser.RegexException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (StringUtil.isBlank(prevStart)) return "";
|
||||||
|
long nextStart = 0;
|
||||||
|
try {
|
||||||
|
nextStart = Long.parseLong(prevStart) + ITEMS_PER_PAGE;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextStart >= total) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + nextStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,7 @@ public class PeertubeService extends StreamingService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListLinkHandlerFactory getPlaylistLHFactory() {
|
public ListLinkHandlerFactory getPlaylistLHFactory() {
|
||||||
// TODO Auto-generated method stub
|
return PeertubePlaylistLinkHandlerFactory.getInstance();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -70,7 +69,6 @@ public class PeertubeService extends StreamingService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SubscriptionExtractor getSubscriptionExtractor() {
|
public SubscriptionExtractor getSubscriptionExtractor() {
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +86,7 @@ public class PeertubeService extends StreamingService {
|
||||||
@Override
|
@Override
|
||||||
public PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler)
|
public PlaylistExtractor getPlaylistExtractor(ListLinkHandler linkHandler)
|
||||||
throws ExtractionException {
|
throws ExtractionException {
|
||||||
// TODO Auto-generated method stub
|
return new PeertubePlaylistExtractor(this, linkHandler);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -16,17 +16,12 @@ import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
|
||||||
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PeertubeAccountExtractor extends ChannelExtractor {
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
private static final String START_KEY = "start";
|
public class PeertubeAccountExtractor extends ChannelExtractor {
|
||||||
private static final String COUNT_KEY = "count";
|
|
||||||
private static final int ITEMS_PER_PAGE = 12;
|
|
||||||
private static final String START_PATTERN = "start=(\\d*)";
|
|
||||||
|
|
||||||
private InfoItemsPage<StreamInfoItem> initPage;
|
private InfoItemsPage<StreamInfoItem> initPage;
|
||||||
private long total;
|
private long total;
|
||||||
|
@ -135,36 +130,12 @@ public class PeertubeAccountExtractor extends ChannelExtractor {
|
||||||
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
PeertubeParsingHelper.validate(json);
|
PeertubeParsingHelper.validate(json);
|
||||||
Number number = JsonUtils.getNumber(json, "total");
|
total = JsonUtils.getNumber(json, "total").longValue();
|
||||||
if (number != null) this.total = number.longValue();
|
|
||||||
collectStreamsFrom(collector, json, pageUrl);
|
collectStreamsFrom(collector, json, pageUrl);
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to get PeerTube kiosk info");
|
throw new ExtractionException("Unable to get PeerTube kiosk info");
|
||||||
}
|
}
|
||||||
return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl));
|
return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String getNextPageUrl(String prevPageUrl) {
|
|
||||||
String prevStart;
|
|
||||||
try {
|
|
||||||
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
|
||||||
} catch (RegexException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (StringUtil.isBlank(prevStart)) return "";
|
|
||||||
long nextStart = 0;
|
|
||||||
try {
|
|
||||||
nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStart >= total) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -21,12 +21,9 @@ import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PeertubeChannelExtractor extends ChannelExtractor {
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
private static final String START_KEY = "start";
|
public class PeertubeChannelExtractor extends ChannelExtractor {
|
||||||
private static final String COUNT_KEY = "count";
|
|
||||||
private static final int ITEMS_PER_PAGE = 12;
|
|
||||||
private static final String START_PATTERN = "start=(\\d*)";
|
|
||||||
|
|
||||||
private InfoItemsPage<StreamInfoItem> initPage;
|
private InfoItemsPage<StreamInfoItem> initPage;
|
||||||
private long total;
|
private long total;
|
||||||
|
@ -141,36 +138,12 @@ public class PeertubeChannelExtractor extends ChannelExtractor {
|
||||||
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
PeertubeParsingHelper.validate(json);
|
PeertubeParsingHelper.validate(json);
|
||||||
Number number = JsonUtils.getNumber(json, "total");
|
this.total = JsonUtils.getNumber(json, "total").longValue();
|
||||||
if (number != null) this.total = number.longValue();
|
|
||||||
collectStreamsFrom(collector, json, pageUrl);
|
collectStreamsFrom(collector, json, pageUrl);
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to get PeerTube kiosk info");
|
throw new ExtractionException("Unable to get PeerTube kiosk info");
|
||||||
}
|
}
|
||||||
return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl));
|
return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String getNextPageUrl(String prevPageUrl) {
|
|
||||||
String prevStart;
|
|
||||||
try {
|
|
||||||
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
|
||||||
} catch (RegexException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (StringUtil.isBlank(prevStart)) return "";
|
|
||||||
long nextStart = 0;
|
|
||||||
try {
|
|
||||||
nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStart >= total) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -182,8 +155,7 @@ public class PeertubeChannelExtractor extends ChannelExtractor {
|
||||||
throw new ExtractionException("Unable to extract PeerTube channel data");
|
throw new ExtractionException("Unable to extract PeerTube channel data");
|
||||||
}
|
}
|
||||||
|
|
||||||
String pageUrl = getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE;
|
this.initPage = getPage(getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE);
|
||||||
this.initPage = getPage(pageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInitialData(String responseBody) throws ExtractionException {
|
private void setInitialData(String responseBody) throws ExtractionException {
|
||||||
|
|
|
@ -13,18 +13,16 @@ import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
import org.schabi.newpipe.extractor.utils.Parser;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PeertubeCommentsExtractor extends CommentsExtractor {
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
private static final String START_KEY = "start";
|
public class PeertubeCommentsExtractor extends CommentsExtractor {
|
||||||
private static final String COUNT_KEY = "count";
|
|
||||||
private static final int ITEMS_PER_PAGE = 12;
|
|
||||||
private static final String START_PATTERN = "start=(\\d*)";
|
|
||||||
|
|
||||||
private InfoItemsPage<CommentsInfoItem> initPage;
|
private InfoItemsPage<CommentsInfoItem> initPage;
|
||||||
private long total;
|
private long total;
|
||||||
|
@ -83,35 +81,12 @@ public class PeertubeCommentsExtractor extends CommentsExtractor {
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to get peertube comments info");
|
throw new ExtractionException("Unable to get peertube comments info");
|
||||||
}
|
}
|
||||||
return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl));
|
return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
||||||
String pageUrl = getUrl() + "?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE;
|
this.initPage = getPage(getUrl() + "?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE);
|
||||||
this.initPage = getPage(pageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getNextPageUrl(String prevPageUrl) {
|
|
||||||
String prevStart;
|
|
||||||
try {
|
|
||||||
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
|
||||||
} catch (RegexException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (StringUtil.isBlank(prevStart)) return "";
|
|
||||||
long nextStart = 0;
|
|
||||||
try {
|
|
||||||
nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStart >= total) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +1,135 @@
|
||||||
package org.schabi.newpipe.extractor.services.peertube.extractors;
|
package org.schabi.newpipe.extractor.services.peertube.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.StreamingService;
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
import org.schabi.newpipe.extractor.playlist.PlaylistExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
public class PeertubePlaylistExtractor extends PlaylistExtractor {
|
public class PeertubePlaylistExtractor extends PlaylistExtractor {
|
||||||
|
|
||||||
|
private JsonObject playlistInfo;
|
||||||
|
private JsonObject playlistVideos;
|
||||||
|
private String initialPageUrl;
|
||||||
|
|
||||||
|
private long total;
|
||||||
|
|
||||||
public PeertubePlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
public PeertubePlaylistExtractor(StreamingService service, ListLinkHandler linkHandler) {
|
||||||
super(service, linkHandler);
|
super(service, linkHandler);
|
||||||
// TODO Auto-generated constructor stub
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getThumbnailUrl() throws ParsingException {
|
public String getThumbnailUrl() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return getBaseUrl() + playlistInfo.getString("thumbnailPath");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBannerUrl() throws ParsingException {
|
public String getBannerUrl() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderUrl() throws ParsingException {
|
public String getUploaderUrl() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return playlistInfo.getObject("ownerAccount").getString("url");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderName() throws ParsingException {
|
public String getUploaderName() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return playlistInfo.getObject("ownerAccount").getString("displayName");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUploaderAvatarUrl() throws ParsingException {
|
public String getUploaderAvatarUrl() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return getBaseUrl() + playlistInfo.getObject("ownerAccount").getObject("avatar").getString("path");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getStreamCount() throws ParsingException {
|
public long getStreamCount() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return playlistInfo.getNumber("videosLength").longValue();
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelName() throws ParsingException {
|
||||||
|
return playlistInfo.getObject("videoChannel").getString("displayName");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelUrl() throws ParsingException {
|
||||||
|
return playlistInfo.getObject("videoChannel").getString("url");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelAvatarUrl() throws ParsingException {
|
||||||
|
return getBaseUrl() + playlistInfo.getObject("videoChannel").getObject("avatar").getString("path");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
// TODO Auto-generated method stub
|
return getPage(initialPageUrl);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getNextPageUrl() throws IOException, ExtractionException {
|
public String getNextPageUrl() throws IOException, ExtractionException {
|
||||||
// TODO Auto-generated method stub
|
return PeertubeParsingHelper.getNextPageUrl(initialPageUrl, total);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getPage(String pageUrl) throws IOException, ExtractionException {
|
public InfoItemsPage<StreamInfoItem> getPage(String pageUrl) throws IOException, ExtractionException {
|
||||||
// TODO Auto-generated method stub
|
Response response = getDownloader().get(pageUrl);
|
||||||
return null;
|
try {
|
||||||
|
playlistVideos = JsonParser.object().from(response.responseBody());
|
||||||
|
} catch (JsonParserException jpe) {
|
||||||
|
throw new ExtractionException("Could not parse json", jpe);
|
||||||
|
}
|
||||||
|
PeertubeParsingHelper.validate(playlistVideos);
|
||||||
|
|
||||||
|
this.total = JsonUtils.getNumber(playlistVideos, "total").longValue();
|
||||||
|
|
||||||
|
StreamInfoItemsCollector collector = new StreamInfoItemsCollector(getServiceId());
|
||||||
|
|
||||||
|
JsonArray videos = playlistVideos.getArray("data");
|
||||||
|
for (Object o : videos) {
|
||||||
|
JsonObject video = ((JsonObject) o).getObject("video");
|
||||||
|
collector.commit(new PeertubeStreamInfoItemExtractor(video, getBaseUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(@Nonnull Downloader downloader) throws IOException, ExtractionException {
|
||||||
|
Response response = downloader.get(getUrl());
|
||||||
|
try {
|
||||||
|
playlistInfo = JsonParser.object().from(response.responseBody());
|
||||||
|
} catch (JsonParserException jpe) {
|
||||||
|
throw new ExtractionException("Could not parse json", jpe);
|
||||||
|
}
|
||||||
|
PeertubeParsingHelper.validate(playlistInfo);
|
||||||
|
initialPageUrl = getUrl() + "/videos?" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public String getName() throws ParsingException {
|
public String getName() throws ParsingException {
|
||||||
// TODO Auto-generated method stub
|
return playlistInfo.getString("displayName");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,18 +15,16 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
|
||||||
import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
|
import org.schabi.newpipe.extractor.search.InfoItemsSearchCollector;
|
||||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
import org.schabi.newpipe.extractor.utils.Parser;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PeertubeSearchExtractor extends SearchExtractor {
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
private static final String START_KEY = "start";
|
public class PeertubeSearchExtractor extends SearchExtractor {
|
||||||
private static final String COUNT_KEY = "count";
|
|
||||||
private static final int ITEMS_PER_PAGE = 12;
|
|
||||||
private static final String START_PATTERN = "start=(\\d*)";
|
|
||||||
|
|
||||||
private InfoItemsPage<InfoItem> initPage;
|
private InfoItemsPage<InfoItem> initPage;
|
||||||
private long total;
|
private long total;
|
||||||
|
@ -88,9 +86,8 @@ public class PeertubeSearchExtractor extends SearchExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
Number number = JsonUtils.getNumber(json, "total");
|
total = JsonUtils.getNumber(json, "total").longValue();
|
||||||
if (number != null) this.total = number.longValue();
|
return new InfoItemsPage<>(collectStreamsFrom(json), PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
return new InfoItemsPage<>(collectStreamsFrom(json), getNextPageUrl(pageUrl));
|
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to get peertube search info");
|
throw new ExtractionException("Unable to get peertube search info");
|
||||||
}
|
}
|
||||||
|
@ -98,31 +95,7 @@ public class PeertubeSearchExtractor extends SearchExtractor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
||||||
String pageUrl = getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE;
|
initPage = getPage(getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE);
|
||||||
this.initPage = getPage(pageUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getNextPageUrl(String prevPageUrl) {
|
|
||||||
String prevStart;
|
|
||||||
try {
|
|
||||||
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
|
||||||
} catch (RegexException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (StringUtil.isBlank(prevStart)) return "";
|
|
||||||
long nextStart = 0;
|
|
||||||
try {
|
|
||||||
nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStart >= total) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,20 +11,16 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
import org.schabi.newpipe.extractor.kiosk.KioskExtractor;
|
||||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
import org.schabi.newpipe.extractor.stream.StreamInfoItemsCollector;
|
||||||
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
import org.schabi.newpipe.extractor.utils.JsonUtils;
|
||||||
import org.schabi.newpipe.extractor.utils.Parser;
|
|
||||||
import org.schabi.newpipe.extractor.utils.Parser.RegexException;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class PeertubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
import static org.schabi.newpipe.extractor.services.peertube.PeertubeParsingHelper.*;
|
||||||
|
|
||||||
private static final String START_KEY = "start";
|
public class PeertubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
||||||
private static final String COUNT_KEY = "count";
|
|
||||||
private static final int ITEMS_PER_PAGE = 12;
|
|
||||||
private static final String START_PATTERN = "start=(\\d*)";
|
|
||||||
|
|
||||||
private InfoItemsPage<StreamInfoItem> initPage;
|
private InfoItemsPage<StreamInfoItem> initPage;
|
||||||
private long total;
|
private long total;
|
||||||
|
@ -89,35 +85,12 @@ public class PeertubeTrendingExtractor extends KioskExtractor<StreamInfoItem> {
|
||||||
} else {
|
} else {
|
||||||
throw new ExtractionException("Unable to get peertube kiosk info");
|
throw new ExtractionException("Unable to get peertube kiosk info");
|
||||||
}
|
}
|
||||||
return new InfoItemsPage<>(collector, getNextPageUrl(pageUrl));
|
return new InfoItemsPage<>(collector, PeertubeParsingHelper.getNextPageUrl(pageUrl, total));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
public void onFetchPage(Downloader downloader) throws IOException, ExtractionException {
|
||||||
String pageUrl = getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE;
|
this.initPage = getPage(getUrl() + "&" + START_KEY + "=0&" + COUNT_KEY + "=" + ITEMS_PER_PAGE);
|
||||||
this.initPage = getPage(pageUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getNextPageUrl(String prevPageUrl) {
|
|
||||||
String prevStart;
|
|
||||||
try {
|
|
||||||
prevStart = Parser.matchGroup1(START_PATTERN, prevPageUrl);
|
|
||||||
} catch (RegexException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (StringUtil.isBlank(prevStart)) return "";
|
|
||||||
long nextStart = 0;
|
|
||||||
try {
|
|
||||||
nextStart = Long.valueOf(prevStart) + ITEMS_PER_PAGE;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextStart >= total) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return prevPageUrl.replace(START_KEY + "=" + prevStart, START_KEY + "=" + String.valueOf(nextStart));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,7 @@ import java.util.List;
|
||||||
public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||||
|
|
||||||
private static final PeertubePlaylistLinkHandlerFactory instance = new PeertubePlaylistLinkHandlerFactory();
|
private static final PeertubePlaylistLinkHandlerFactory instance = new PeertubePlaylistLinkHandlerFactory();
|
||||||
private static final String ID_PATTERN = "/video-channels/([^/?&#]*)";
|
private static final String ID_PATTERN = "/videos/watch/playlist/([^/?&#]*)";
|
||||||
private static final String VIDEO_CHANNELS_ENDPOINT = "/api/v1/video-channels/";
|
|
||||||
|
|
||||||
public static PeertubePlaylistLinkHandlerFactory getInstance() {
|
public static PeertubePlaylistLinkHandlerFactory getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -26,7 +25,7 @@ public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl(String id, List<String> contentFilters, String sortFilter, String baseUrl) {
|
public String getUrl(String id, List<String> contentFilters, String sortFilter, String baseUrl) {
|
||||||
return baseUrl + VIDEO_CHANNELS_ENDPOINT + id;
|
return baseUrl + "/api/v1/video-playlists/" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,9 +33,13 @@ public class PeertubePlaylistLinkHandlerFactory extends ListLinkHandlerFactory {
|
||||||
return Parser.matchGroup1(ID_PATTERN, url);
|
return Parser.matchGroup1(ID_PATTERN, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onAcceptUrl(final String url) {
|
public boolean onAcceptUrl(final String url) {
|
||||||
return url.contains("/video-channels/");
|
try {
|
||||||
|
getId(url);
|
||||||
|
return true;
|
||||||
|
} catch (ParsingException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,12 @@ public class PeertubeStreamLinkHandlerFactory extends LinkHandlerFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onAcceptUrl(final String url) throws FoundAdException {
|
public boolean onAcceptUrl(final String url) throws FoundAdException {
|
||||||
return url.contains("/videos/");
|
if (url.contains("/playlist/")) return false;
|
||||||
|
try {
|
||||||
|
getId(url);
|
||||||
|
return true;
|
||||||
|
} catch (ParsingException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,24 @@ public class SoundcloudPlaylistExtractor extends PlaylistExtractor {
|
||||||
return playlist.getNumber("track_count", 0).longValue();
|
return playlist.getNumber("track_count", 0).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelName() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelAvatarUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
public InfoItemsPage<StreamInfoItem> getInitialPage() throws IOException, ExtractionException {
|
||||||
|
|
|
@ -150,6 +150,24 @@ public class YoutubePlaylistExtractor extends PlaylistExtractor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelName() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public String getSubChannelAvatarUrl() throws ParsingException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@Override
|
@Override
|
||||||
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
public InfoItemsPage<StreamInfoItem> getInitialPage() {
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package org.schabi.newpipe.extractor.services.peertube;
|
||||||
|
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.schabi.newpipe.DownloaderTestImpl;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.extractors.PeertubePlaylistExtractor;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.schabi.newpipe.extractor.ServiceList.PeerTube;
|
||||||
|
|
||||||
|
public class PeertubePlaylistExtractorTest {
|
||||||
|
|
||||||
|
public static class Shocking {
|
||||||
|
private static PeertubePlaylistExtractor extractor;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUp() throws Exception {
|
||||||
|
NewPipe.init(DownloaderTestImpl.getInstance());
|
||||||
|
extractor = (PeertubePlaylistExtractor) PeerTube
|
||||||
|
.getPlaylistExtractor("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7");
|
||||||
|
extractor.fetchPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetName() throws ParsingException {
|
||||||
|
assertEquals("Shocking !", extractor.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetThumbnailUrl() throws ParsingException {
|
||||||
|
assertEquals("https://framatube.org/static/thumbnails/playlist-96b0ee2b-a5a7-4794-8769-58d8ccb79ab7.jpg", extractor.getThumbnailUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUploaderUrl() throws ParsingException {
|
||||||
|
assertEquals("https://skeptikon.fr/accounts/metadechoc", extractor.getUploaderUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUploaderAvatarUrl() throws ParsingException {
|
||||||
|
assertEquals("https://framatube.org/lazy-static/avatars/cd0f781d-0287-4be2-94f1-24cd732337b2.jpg", extractor.getUploaderAvatarUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUploaderName() throws ParsingException {
|
||||||
|
assertEquals("Méta de Choc", extractor.getUploaderName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetStreamCount() throws ParsingException {
|
||||||
|
assertEquals(35, extractor.getStreamCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSubChannelUrl() throws ParsingException {
|
||||||
|
assertEquals("https://skeptikon.fr/video-channels/metadechoc_channel", extractor.getSubChannelUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSubChannelName() throws ParsingException {
|
||||||
|
assertEquals("SHOCKING !", extractor.getSubChannelName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSubChannelAvatarUrl() throws ParsingException {
|
||||||
|
assertEquals("https://framatube.org/lazy-static/avatars/f1dcd0e8-e651-42ed-ae81-bb3bd4aff2bc.png",
|
||||||
|
extractor.getSubChannelAvatarUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,12 +25,17 @@ public class PeertubePlaylistLinkHandlerFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void acceptUrlTest() throws ParsingException {
|
public void acceptUrlTest() throws ParsingException {
|
||||||
assertTrue(linkHandler.acceptUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33/videos"));
|
assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909"));
|
||||||
|
assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909/videos"));
|
||||||
|
assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/dacdc4ef-5160-4846-9b70-a655880da667"));
|
||||||
|
assertTrue(linkHandler.acceptUrl("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getIdFromUrl() throws ParsingException {
|
public void getIdFromUrl() throws ParsingException {
|
||||||
assertEquals("b45e84fb-c47f-475b-94f2-718126154d33", linkHandler.fromUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33").getId());
|
assertEquals("d8ca79f9-e4c7-4269-8183-d78ed269c909", linkHandler.getId("https://framatube.org/videos/watch/playlist/d8ca79f9-e4c7-4269-8183-d78ed269c909"));
|
||||||
assertEquals("b45e84fb-c47f-475b-94f2-718126154d33", linkHandler.fromUrl("https://peertube.mastodon.host/video-channels/b45e84fb-c47f-475b-94f2-718126154d33/videos").getId());
|
assertEquals("dacdc4ef-5160-4846-9b70-a655880da667", linkHandler.getId("https://framatube.org/videos/watch/playlist/dacdc4ef-5160-4846-9b70-a655880da667"));
|
||||||
|
assertEquals("bfc145f5-1be7-48a6-9b9e-4f1967199dad", linkHandler.getId("https://framatube.org/videos/watch/playlist/bfc145f5-1be7-48a6-9b9e-4f1967199dad"));
|
||||||
|
assertEquals("96b0ee2b-a5a7-4794-8769-58d8ccb79ab7", linkHandler.getId("https://framatube.org/videos/watch/playlist/96b0ee2b-a5a7-4794-8769-58d8ccb79ab7"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue