searchfilters: convert youtube to new framework

Available content filters:
    Youtube
    - all
    - videos
    - channels
    - playlists
    Youtube Music
    - Songs
    - Videos
    - Albums
    - Playlists
    - Artists

    Available sort filters:
    - 'Sort by'
    - Upload Date
    - Duration
    - Features
This commit is contained in:
Stypox 2023-12-30 08:43:06 +01:00
parent 8568f196ec
commit 0a4b88955a
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
12 changed files with 874 additions and 126 deletions

View File

@ -1,5 +1,6 @@
plugins {
id 'checkstyle'
id 'com.squareup.wire' version '4.4.1'
}
test {
@ -18,6 +19,15 @@ checkstyle {
toolVersion checkstyleVersion
}
checkstyleMain
// exclude the wire generated youtube protobuf files
.exclude ('org/schabi/newpipe/extractor/services/youtube/search/filter/protobuf/')
wire {
java {
}
}
checkstyleTest {
enabled false // do not checkstyle test files
}

View File

@ -1,5 +1,8 @@
package org.schabi.newpipe.extractor.services.youtube;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
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.StreamingService.ServiceInfo.MediaCapability.LIVE;
@ -46,6 +49,7 @@ import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeTrending
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 java.util.List;
@ -137,9 +141,10 @@ public class YoutubeService extends StreamingService {
@Override
public SearchExtractor getSearchExtractor(final SearchQueryHandler query) {
final List<String> contentFilters = query.getContentFilters();
final FilterItem filterItem =
Utils.getFirstContentFilterItem(query);
if (!contentFilters.isEmpty() && contentFilters.get(0).startsWith("music_")) {
if (filterItem instanceof YoutubeFilters.MusicYoutubeContentFilterItem) {
return new YoutubeMusicSearchExtractor(this, query);
} else {
return new YoutubeSearchExtractor(this, query);

View File

@ -29,14 +29,16 @@ import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@ -49,6 +51,15 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
super(service, linkHandler);
}
private String getSearchType() {
final YoutubeFilters.MusicYoutubeContentFilterItem contentFilterItem =
Utils.getFirstContentFilterItem(getLinkHandler());
if (contentFilterItem != null && contentFilterItem.getName() != null) {
return contentFilterItem.getName();
}
return "";
}
@Override
public void onFetchPage(@Nonnull final Downloader downloader)
throws IOException, ExtractionException {
@ -57,28 +68,12 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
final String url = "https://music.youtube.com/youtubei/v1/search?key="
+ youtubeMusicKeys[0] + DISABLE_PRETTY_PRINT_PARAMETER;
final String params;
switch (getLinkHandler().getContentFilters().get(0)) {
case MUSIC_SONGS:
params = "Eg-KAQwIARAAGAAgACgAMABqChAEEAUQAxAKEAk%3D";
break;
case MUSIC_VIDEOS:
params = "Eg-KAQwIABABGAAgACgAMABqChAEEAUQAxAKEAk%3D";
break;
case MUSIC_ALBUMS:
params = "Eg-KAQwIABAAGAEgACgAMABqChAEEAUQAxAKEAk%3D";
break;
case MUSIC_PLAYLISTS:
params = "Eg-KAQwIABAAGAAgACgBMABqChAEEAUQAxAKEAk%3D";
break;
case MUSIC_ARTISTS:
params = "Eg-KAQwIABAAGAAgASgAMABqChAEEAUQAxAKEAk%3D";
break;
default:
params = null;
break;
}
final YoutubeFilters.MusicYoutubeContentFilterItem contentFilterItem =
Utils.getFirstContentFilterItem(getLinkHandler());
// Get the search parameter for the request. If getParams() be null
// (which should never happen - only in test cases), JsonWriter.string() can handle it
final String params = (contentFilterItem != null) ? contentFilterItem.getParams() : null;
// @formatter:off
final byte[] json = JsonWriter.string()
@ -256,7 +251,7 @@ public class YoutubeMusicSearchExtractor extends SearchExtractor {
private void collectMusicStreamsFrom(final MultiInfoItemsCollector collector,
@Nonnull final JsonArray videos) {
final String searchType = getLinkHandler().getContentFilters().get(0);
final String searchType = getSearchType();
videos.stream()
.filter(JsonObject.class::isInstance)
.map(JsonObject.class::cast)

View File

@ -6,11 +6,6 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getKey;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getTextFromObject;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.prepareDesktopJsonBuilder;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.ALL;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.CHANNELS;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.PLAYLISTS;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.VIDEOS;
import static org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory.getSearchParameter;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import com.grack.nanojson.JsonArray;
@ -30,8 +25,11 @@ import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandler;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.extractor.localization.TimeAgoParser;
import org.schabi.newpipe.extractor.search.SearchExtractor;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.services.youtube.YoutubeMetaInfoHelper;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
import org.schabi.newpipe.extractor.utils.JsonUtils;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -63,8 +61,6 @@ import javax.annotation.Nullable;
public class YoutubeSearchExtractor extends SearchExtractor {
@Nullable
private final String searchType;
private final boolean extractVideoResults;
private final boolean extractChannelResults;
private final boolean extractPlaylistResults;
@ -74,17 +70,20 @@ public class YoutubeSearchExtractor extends SearchExtractor {
public YoutubeSearchExtractor(final StreamingService service,
final SearchQueryHandler linkHandler) {
super(service, linkHandler);
final List<String> contentFilters = linkHandler.getContentFilters();
searchType = isNullOrEmpty(contentFilters) ? null : contentFilters.get(0);
final List<FilterItem> contentFilters = linkHandler.getContentFilters();
// Save whether we should extract video, channel and playlist results depending on the
// requested search type, as YouTube returns sometimes videos inside channel search results
// If no search type is provided or ALL filter is requested, extract everything
extractVideoResults = searchType == null || ALL.equals(searchType)
|| VIDEOS.equals(searchType);
extractChannelResults = searchType == null || ALL.equals(searchType)
|| CHANNELS.equals(searchType);
extractPlaylistResults = searchType == null || ALL.equals(searchType)
|| PLAYLISTS.equals(searchType);
final boolean nullOrAll = contentFilters == null || contentFilters.isEmpty()
|| contentFilters.stream()
.anyMatch(item -> item.getIdentifier() == YoutubeFilters.ID_CF_MAIN_ALL);
extractVideoResults = nullOrAll || contentFilters.stream()
.anyMatch(item -> item.getIdentifier() == YoutubeFilters.ID_CF_MAIN_VIDEOS);
extractChannelResults = nullOrAll || contentFilters.stream()
.anyMatch(item -> item.getIdentifier() == YoutubeFilters.ID_CF_MAIN_CHANNELS);
extractPlaylistResults = nullOrAll || contentFilters.stream()
.anyMatch(item -> item.getIdentifier() == YoutubeFilters.ID_CF_MAIN_PLAYLISTS);
}
@Override
@ -92,7 +91,12 @@ public class YoutubeSearchExtractor extends SearchExtractor {
ExtractionException {
final String query = super.getSearchString();
final Localization localization = getExtractorLocalization();
final String params = getSearchParameter(searchType);
final YoutubeFilters.YoutubeContentFilterItem contentFilterItem =
Utils.getFirstContentFilterItem(getLinkHandler());
// Get the search parameter for the request. If getParams() be null
// (which should never happen - only in test cases), JsonWriter.string() can handle it
final String params = (contentFilterItem != null) ? contentFilterItem.getParams() : null;
final JsonBuilder<JsonObject> jsonBody = prepareDesktopJsonBuilder(localization,
getExtractorContentCountry())

View File

@ -20,6 +20,8 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
@ -53,12 +55,14 @@ public final class YoutubeChannelLinkHandlerFactory extends ListLinkHandlerFacto
* Returns the URL to a channel from an ID.
*
* @param id the channel ID including e.g. 'channel/'
* @param contentFilters
* @param searchFilter
* @return the URL to the channel
*/
@Override
public String getUrl(final String id,
final List<String> contentFilters,
final String searchFilter)
final List<FilterItem> contentFilters,
final List<FilterItem> searchFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com/" + id;
}

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
@ -44,8 +46,8 @@ public final class YoutubeCommentsLinkHandlerFactory extends ListLinkHandlerFact
@Override
public String getUrl(final String id,
final List<String> contentFilter,
final String sortFilter)
final List<FilterItem> contentFilter,
final List<FilterItem> sortFilter)
throws ParsingException, UnsupportedOperationException {
return getUrl(id);
}

View File

@ -8,6 +8,8 @@ import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
@ -25,8 +27,9 @@ public final class YoutubePlaylistLinkHandlerFactory extends ListLinkHandlerFact
}
@Override
public String getUrl(final String id, final List<String> contentFilters,
final String sortFilter)
public String getUrl(final String id,
final List<FilterItem> contentFilters,
final List<FilterItem> sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com/playlist?list=" + id;
}

View File

@ -1,21 +1,16 @@
package org.schabi.newpipe.extractor.services.youtube.linkHandler;
import static org.schabi.newpipe.extractor.utils.Utils.encodeUrlUtf8;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
import java.io.UnsupportedEncodingException;
import java.util.List;
import javax.annotation.Nonnull;
public final class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFactory {
private static final YoutubeSearchQueryHandlerFactory INSTANCE =
new YoutubeSearchQueryHandlerFactory();
public static final String ALL = "all";
public static final String VIDEOS = "videos";
public static final String CHANNELS = "channels";
@ -27,8 +22,12 @@ public final class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFa
public static final String MUSIC_PLAYLISTS = "music_playlists";
public static final String MUSIC_ARTISTS = "music_artists";
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
private static final YoutubeSearchQueryHandlerFactory INSTANCE =
new YoutubeSearchQueryHandlerFactory();
private YoutubeSearchQueryHandlerFactory() {
super(new YoutubeFilters());
}
@Nonnull
public static YoutubeSearchQueryHandlerFactory getInstance() {
@ -37,73 +36,10 @@ public final class YoutubeSearchQueryHandlerFactory extends SearchQueryHandlerFa
@Override
public String getUrl(final String searchString,
@Nonnull final List<String> contentFilters,
final String sortFilter)
throws ParsingException, UnsupportedOperationException {
try {
if (!contentFilters.isEmpty()) {
final String contentFilter = contentFilters.get(0);
switch (contentFilter) {
case VIDEOS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQAfABAQ%253D%253D";
case CHANNELS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQAvABAQ%253D%253D";
case PLAYLISTS:
return SEARCH_URL + encodeUrlUtf8(searchString)
+ "&sp=EgIQA_ABAQ%253D%253D";
case MUSIC_SONGS:
case MUSIC_VIDEOS:
case MUSIC_ALBUMS:
case MUSIC_PLAYLISTS:
case MUSIC_ARTISTS:
return MUSIC_SEARCH_URL + encodeUrlUtf8(searchString);
}
}
return SEARCH_URL + encodeUrlUtf8(searchString) + "&sp=8AEB";
} catch (final UnsupportedEncodingException e) {
throw new ParsingException("Could not encode query", e);
}
}
@Override
public String[] getAvailableContentFilter() {
return new String[]{
ALL,
VIDEOS,
CHANNELS,
PLAYLISTS,
MUSIC_SONGS,
MUSIC_VIDEOS,
MUSIC_ALBUMS,
MUSIC_PLAYLISTS
// MUSIC_ARTISTS
};
}
@Nonnull
public static String getSearchParameter(final String contentFilter) {
if (isNullOrEmpty(contentFilter)) {
return "8AEB";
}
switch (contentFilter) {
case VIDEOS:
return "EgIQAfABAQ%3D%3D";
case CHANNELS:
return "EgIQAvABAQ%3D%3D";
case PLAYLISTS:
return "EgIQA_ABAQ%3D%3D";
case MUSIC_SONGS:
case MUSIC_VIDEOS:
case MUSIC_ALBUMS:
case MUSIC_PLAYLISTS:
case MUSIC_ARTISTS:
return "";
default:
return "8AEB";
}
@Nonnull final List<FilterItem> selectedContentFilter,
final List<FilterItem> selectedSortFilter) throws ParsingException {
searchFilters.setSelectedContentFilter(selectedContentFilter);
searchFilters.setSelectedSortFilter(selectedSortFilter);
return searchFilters.evaluateSelectedFilters(searchString);
}
}

View File

@ -25,6 +25,7 @@ import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.utils.Utils;
import java.net.MalformedURLException;
@ -44,8 +45,8 @@ public final class YoutubeTrendingLinkHandlerFactory extends ListLinkHandlerFact
}
public String getUrl(final String id,
final List<String> contentFilters,
final String sortFilter)
final List<FilterItem> contentFilters,
final List<FilterItem> sortFilter)
throws ParsingException, UnsupportedOperationException {
return "https://www.youtube.com/feed/trending";
}

View File

@ -0,0 +1,504 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.youtube.search.filter;
import org.schabi.newpipe.extractor.search.filter.BaseSearchFilters;
import org.schabi.newpipe.extractor.search.filter.FilterContainer;
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.DateFilter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.Features;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.LengthFilter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.SortOrder;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.TypeFilter;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class YoutubeFilters extends BaseSearchFilters {
public static final String UTF_8 = "UTF-8";
/**
* 'ALL' this is the default search content filter.
* It has all sort filters that are available.
*/
public static final String ALL = "all";
public static final String VIDEOS = "videos";
public static final String CHANNELS = "channels";
public static final String PLAYLISTS = "playlists";
// public static final String MOVIES = "movies";
public static final int ID_CF_MAIN_GRP = 0;
public static final int ID_CF_MAIN_ALL = 1;
public static final int ID_CF_MAIN_VIDEOS = 2;
public static final int ID_CF_MAIN_CHANNELS = 3;
public static final int ID_CF_MAIN_PLAYLISTS = 4;
// public static final int ID_CF_MAIN_MOVIES = 5;
public static final int ID_CF_MAIN_YOUTUBE_MUSIC_SONGS = 6;
public static final int ID_CF_MAIN_YOUTUBE_MUSIC_VIDEOS = 7;
public static final int ID_CF_MAIN_YOUTUBE_MUSIC_ALBUMS = 8;
public static final int ID_CF_MAIN_YOUTUBE_MUSIC_PLAYLISTS = 9;
public static final int ID_CF_MAIN_YOUTUBE_MUSIC_ARTISTS = 10;
public static final int ID_SF_SORT_BY_GRP = 11;
public static final int ID_SF_SORT_BY_RELEVANCE = 12;
public static final int ID_SF_SORT_BY_RATING = 13;
public static final int ID_SF_SORT_BY_DATE = 14;
public static final int ID_SF_SORT_BY_VIEWS = 15;
public static final int ID_SF_UPLOAD_DATE_GRP = 16;
public static final int ID_SF_UPLOAD_DATE_ALL = 17;
public static final int ID_SF_UPLOAD_DATE_HOUR = 18;
public static final int ID_SF_UPLOAD_DATE_DAY = 19;
public static final int ID_SF_UPLOAD_DATE_WEEK = 20;
public static final int ID_SF_UPLOAD_DATE_MONTH = 21;
public static final int ID_SF_UPLOAD_DATE_YEAR = 22;
public static final int ID_SF_DURATION_GRP = 23;
public static final int ID_SF_DURATION_ALL = 24;
public static final int ID_SF_DURATION_SHORT = 25;
public static final int ID_SF_DURATION_MEDIUM = 26;
public static final int ID_SF_DURATION_LONG = 27;
public static final int ID_SF_FEATURES_GRP = 28;
public static final int ID_SF_FEATURES_LIVE = 29;
public static final int ID_SF_FEATURES_4K = 30;
public static final int ID_SF_FEATURES_HD = 31;
public static final int ID_SF_FEATURES_SUBTITLES = 32;
public static final int ID_SF_FEATURES_CCOMMONS = 33;
public static final int ID_SF_FEATURES_360 = 34;
public static final int ID_SF_FEATURES_VR180 = 35;
public static final int ID_SF_FEATURES_3D = 36;
public static final int ID_SF_FEATURES_HDR = 37;
public static final int ID_SF_FEATURES_LOCATION = 38;
public static final int ID_SF_FEATURES_PURCHASED = 39;
public static final String MUSIC_SONGS = "music_songs";
public static final String MUSIC_VIDEOS = "music_videos";
public static final String MUSIC_ALBUMS = "music_albums";
public static final String MUSIC_PLAYLISTS = "music_playlists";
public static final String MUSIC_ARTISTS = "music_artists";
private static final String SEARCH_URL = "https://www.youtube.com/results?search_query=";
private static final String MUSIC_SEARCH_URL = "https://music.youtube.com/search?q=";
/**
* generate the search parameter protobuf 'sp' string that is appended to the search URL.
*
* @param contentFilterItem the active content filter item
* @return the protobuf base64 encoded 'sp' parameter
*/
private String generateYoutubeSpParameter(final YoutubeContentFilterItem contentFilterItem) {
boolean atLeastOneParamSet = false;
final YoutubeProtoBufferSearchParameterAccessor.Builder builder =
new YoutubeProtoBufferSearchParameterAccessor.Builder();
final TypeFilter typeFilter = (contentFilterItem != null)
? contentFilterItem.getContentType()
: null;
// set content filter item in builder
if (contentFilterItem != null) {
atLeastOneParamSet = true;
builder.setTypeFilter(typeFilter);
}
if (selectedSortFilter != null) {
for (final FilterItem sortItem : selectedSortFilter) {
if (checkSortFilterItemAndSetInBuilder(builder, sortItem)) {
atLeastOneParamSet = true;
}
}
}
try {
if (atLeastOneParamSet) {
return builder.build().getSp();
} else {
return null;
}
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
/**
* Check if suitable youtube sort filter and set it in the builder.
*
* @param builder the builder for protobuf
* @param sortItem the item to check and add
* @return true if item was set in the builder
*/
private boolean checkSortFilterItemAndSetInBuilder(
final YoutubeProtoBufferSearchParameterAccessor.Builder builder,
final FilterItem sortItem) {
boolean atLeastOneParamSet = false;
if (sortItem instanceof YoutubeSortOrderSortFilterItem) {
final SortOrder sortOrder = ((YoutubeSortOrderSortFilterItem) sortItem).get();
if (null != sortOrder) {
builder.setSortOrder(sortOrder);
atLeastOneParamSet = true;
}
} else if (sortItem instanceof YoutubeDateSortFilterItem) {
final DateFilter dateFilter = ((YoutubeDateSortFilterItem) sortItem).get();
if (null != dateFilter) {
builder.setDateFilter(dateFilter);
atLeastOneParamSet = true;
}
} else if (sortItem instanceof YoutubeLenSortFilterItem) {
final LengthFilter lengthFilter = ((YoutubeLenSortFilterItem) sortItem).get();
if (null != lengthFilter) {
builder.setLengthFilter(lengthFilter);
atLeastOneParamSet = true;
}
} else if (sortItem instanceof YoutubeFeatureSortFilterItem) {
final Features feature = ((YoutubeFeatureSortFilterItem) sortItem).get();
if (null != feature) {
builder.addFeature(feature);
atLeastOneParamSet = true;
}
}
return atLeastOneParamSet;
}
@Override
public String evaluateSelectedFilters(final String searchString) {
String sp = null;
if (selectedContentFilter != null && !selectedContentFilter.isEmpty()) {
// as of now there is just one content filter available
final YoutubeContentFilterItem contentFilterItem =
(YoutubeContentFilterItem) selectedContentFilter.get(0);
sp = generateYoutubeSpParameter(contentFilterItem);
if (contentFilterItem instanceof MusicYoutubeContentFilterItem) {
try {
return MUSIC_SEARCH_URL
+ Utils.encodeUrlUtf8(searchString);
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
} else {
if (contentFilterItem != null) {
contentFilterItem.setParams(sp);
}
}
}
try {
return SEARCH_URL
+ Utils.encodeUrlUtf8(searchString)
+ ((null != sp && !sp.isEmpty()) ? "&sp=" + sp : "");
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings("checkstyle:MethodLength")
@Override
protected void init() {
/* sort filters */
/* 'Sort order' filter items */
groupsFactory.addFilterItem(new YoutubeSortOrderSortFilterItem(
ID_SF_SORT_BY_RELEVANCE, "Relevance", SortOrder.relevance));
groupsFactory.addFilterItem(new YoutubeSortOrderSortFilterItem(
ID_SF_SORT_BY_RATING, "Rating", SortOrder.rating));
groupsFactory.addFilterItem(new YoutubeSortOrderSortFilterItem(
ID_SF_SORT_BY_DATE, "Date", SortOrder.date));
groupsFactory.addFilterItem(new YoutubeSortOrderSortFilterItem(
ID_SF_SORT_BY_VIEWS, "Views", SortOrder.views));
/* 'Date' filter items */
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_ALL, "All", null));
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_HOUR, "Hour", DateFilter.hour));
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_DAY, "Day", DateFilter.day));
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_WEEK, "Week", DateFilter.week));
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_MONTH, "Month", DateFilter.month));
groupsFactory.addFilterItem(new YoutubeDateSortFilterItem(
ID_SF_UPLOAD_DATE_YEAR, "Year", DateFilter.year));
/* 'Duration' filter items */
groupsFactory.addFilterItem(new YoutubeLenSortFilterItem(
ID_SF_DURATION_ALL, "All", null));
groupsFactory.addFilterItem(new YoutubeLenSortFilterItem(
ID_SF_DURATION_SHORT, "Under 4 min", LengthFilter.duration_short));
groupsFactory.addFilterItem(new YoutubeLenSortFilterItem(
ID_SF_DURATION_MEDIUM, "4-20 min", LengthFilter.duration_medium));
groupsFactory.addFilterItem(new YoutubeLenSortFilterItem(
ID_SF_DURATION_LONG, "Over 20 min", LengthFilter.duration_long));
/* 'features' filter items */
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_LIVE, "Live", Features.live));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_4K, "4k", Features.is_4k));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_HD, "HD", Features.is_hd));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_SUBTITLES, "Subtitles", Features.subtitles));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_CCOMMONS, "Ccommons", Features.ccommons));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_360, "360°", Features.is_360));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_VR180, "VR180", Features.is_vr180));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_3D, "3d", Features.is_3d));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_HDR, "Hdr", Features.is_hdr));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_LOCATION, "Location", Features.location));
groupsFactory.addFilterItem(new YoutubeFeatureSortFilterItem(
ID_SF_FEATURES_PURCHASED, "Purchased", Features.purchased));
final FilterGroup sortByGroup =
groupsFactory.createFilterGroup(ID_SF_SORT_BY_GRP, "Sort by", true,
ID_SF_SORT_BY_RELEVANCE, new FilterItem[]{
groupsFactory.getFilterForId(ID_SF_SORT_BY_RELEVANCE),
groupsFactory.getFilterForId(ID_SF_SORT_BY_RATING),
groupsFactory.getFilterForId(ID_SF_SORT_BY_DATE),
groupsFactory.getFilterForId(ID_SF_SORT_BY_VIEWS),
}, null);
final FilterGroup uploadDateGroup =
groupsFactory.createFilterGroup(ID_SF_UPLOAD_DATE_GRP, "Upload Date", true,
ID_SF_UPLOAD_DATE_ALL, new FilterItem[]{
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_ALL),
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_HOUR),
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_DAY),
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_WEEK),
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_MONTH),
groupsFactory.getFilterForId(ID_SF_UPLOAD_DATE_YEAR),
}, null);
final FilterGroup durationGroup =
groupsFactory.createFilterGroup(ID_SF_DURATION_GRP, "Duration", true,
ID_SF_DURATION_ALL, new FilterItem[]{
groupsFactory.getFilterForId(ID_SF_DURATION_ALL),
groupsFactory.getFilterForId(ID_SF_DURATION_SHORT),
groupsFactory.getFilterForId(ID_SF_DURATION_MEDIUM),
groupsFactory.getFilterForId(ID_SF_DURATION_LONG),
}, null);
final FilterGroup featureGroup =
groupsFactory.createFilterGroup(ID_SF_FEATURES_GRP, "Features", false,
FilterContainer.ITEM_IDENTIFIER_UNKNOWN, new FilterItem[]{
groupsFactory.getFilterForId(ID_SF_FEATURES_LIVE),
groupsFactory.getFilterForId(ID_SF_FEATURES_4K),
groupsFactory.getFilterForId(ID_SF_FEATURES_HD),
groupsFactory.getFilterForId(ID_SF_FEATURES_SUBTITLES),
groupsFactory.getFilterForId(ID_SF_FEATURES_CCOMMONS),
groupsFactory.getFilterForId(ID_SF_FEATURES_360),
groupsFactory.getFilterForId(ID_SF_FEATURES_VR180),
groupsFactory.getFilterForId(ID_SF_FEATURES_3D),
groupsFactory.getFilterForId(ID_SF_FEATURES_HDR),
groupsFactory.getFilterForId(ID_SF_FEATURES_LOCATION),
// there is not use for that feature ATM.
// groupsFactory.getFilterForId(ID_SF_FEATURES_PURCHASED),
}, null);
final FilterGroup[] videoFilters = new FilterGroup[]{
sortByGroup,
uploadDateGroup,
durationGroup,
featureGroup
};
final FilterGroup[] channelPlaylistFilters = new FilterGroup[]{sortByGroup};
/* videoFilters contains all sort filters available */
final FilterContainer allSortFilters = new FilterContainer(videoFilters);
final FilterContainer sortFiltersForChannelAndPlaylists =
new FilterContainer(channelPlaylistFilters);
addContentFilterTypeAndSortVariant(ID_CF_MAIN_ALL, allSortFilters);
addContentFilterTypeAndSortVariant(ID_CF_MAIN_VIDEOS, allSortFilters);
addContentFilterTypeAndSortVariant(ID_CF_MAIN_CHANNELS, sortFiltersForChannelAndPlaylists);
addContentFilterTypeAndSortVariant(ID_CF_MAIN_PLAYLISTS, sortFiltersForChannelAndPlaylists);
/*
// -> movies are only available for logged in users
addContentFilterTypeAndSortVariant(ID_CF_MAIN_MOVIES, new FilterContainer(new FilterGroup[]{
sortByGroup,
uploadDateGroup,
durationGroup,
groupsFactory.createSortGroup(ID_SF_FEATURES_GRP, "Features", false,
Filter.ITEM_IDENTIFIER_UNKNOWN, new FilterItem[]{
groupsFactory.getFilterForId(ID_SF_FEATURES_4K),
groupsFactory.getFilterForId(ID_SF_FEATURES_HD),
groupsFactory.getFilterForId(ID_SF_FEATURES_360),
groupsFactory.getFilterForId(ID_SF_FEATURES_VR180),
groupsFactory.getFilterForId(ID_SF_FEATURES_HDR),
groupsFactory.getFilterForId(ID_SF_FEATURES_LOCATION),
// there is not use for that feature ATM.
// groupsFactory.getFilterForId(ID_SF_FEATURES_PURCHASED),
}, null)
});
*/
/* content filters with sort filters */
groupsFactory.addFilterItem(new YoutubeContentFilterItem(
ID_CF_MAIN_ALL, ALL, null));
groupsFactory.addFilterItem(new YoutubeContentFilterItem(
ID_CF_MAIN_VIDEOS, VIDEOS, TypeFilter.video));
groupsFactory.addFilterItem(new YoutubeContentFilterItem(
ID_CF_MAIN_CHANNELS, CHANNELS, TypeFilter.channel));
groupsFactory.addFilterItem(new YoutubeContentFilterItem(
ID_CF_MAIN_PLAYLISTS, PLAYLISTS, TypeFilter.playlist));
/*
// movies are only available for logged in users
builder.addFilterItem(new YoutubeContentFilterItem(
ID_CF_MAIN_MOVIES, MOVIES, TypeFilter.movie));
*/
/* Youtube Music content filters */
groupsFactory.addFilterItem(new MusicYoutubeContentFilterItem(
ID_CF_MAIN_YOUTUBE_MUSIC_SONGS, MUSIC_SONGS,
"Eg-KAQwIARAAGAAgACgAMABqChAEEAUQAxAKEAk%3D"
));
groupsFactory.addFilterItem(new MusicYoutubeContentFilterItem(
ID_CF_MAIN_YOUTUBE_MUSIC_VIDEOS, MUSIC_VIDEOS,
"Eg-KAQwIABABGAAgACgAMABqChAEEAUQAxAKEAk%3D"
));
groupsFactory.addFilterItem(new MusicYoutubeContentFilterItem(
ID_CF_MAIN_YOUTUBE_MUSIC_ALBUMS, MUSIC_ALBUMS,
"Eg-KAQwIABAAGAEgACgAMABqChAEEAUQAxAKEAk%3D"
));
groupsFactory.addFilterItem(new MusicYoutubeContentFilterItem(
ID_CF_MAIN_YOUTUBE_MUSIC_PLAYLISTS, MUSIC_PLAYLISTS,
"Eg-KAQwIABAAGAAgACgBMABqChAEEAUQAxAKEAk%3D"
));
groupsFactory.addFilterItem(new MusicYoutubeContentFilterItem(
ID_CF_MAIN_YOUTUBE_MUSIC_ARTISTS, MUSIC_ARTISTS,
"Eg-KAQwIABAAGAAgASgAMABqChAEEAUQAxAKEAk%3D"
));
final FilterGroup contentFilterGroup =
groupsFactory.createFilterGroup(ID_CF_MAIN_GRP, null, true,
ID_CF_MAIN_ALL, new FilterItem[]{
groupsFactory.getFilterForId(ID_CF_MAIN_ALL),
groupsFactory.getFilterForId(ID_CF_MAIN_VIDEOS),
groupsFactory.getFilterForId(ID_CF_MAIN_CHANNELS),
groupsFactory.getFilterForId(ID_CF_MAIN_PLAYLISTS),
// groupsFactory.getFilterForId(ID_CF_MAIN_MOVIES),
groupsFactory.getFilterForId(groupsFactory.addFilterItem(
new FilterItem.DividerItem("YouTube Music"))),
groupsFactory.getFilterForId(ID_CF_MAIN_YOUTUBE_MUSIC_SONGS),
groupsFactory.getFilterForId(ID_CF_MAIN_YOUTUBE_MUSIC_VIDEOS),
groupsFactory.getFilterForId(ID_CF_MAIN_YOUTUBE_MUSIC_ALBUMS),
groupsFactory.getFilterForId(ID_CF_MAIN_YOUTUBE_MUSIC_PLAYLISTS),
groupsFactory.getFilterForId(ID_CF_MAIN_YOUTUBE_MUSIC_ARTISTS),
}, allSortFilters);
addContentFilterGroup(contentFilterGroup);
}
private void addContentFilterTypeAndSortVariant(final int contentFilterId,
final FilterContainer variant) {
addContentFilterSortVariant(contentFilterId, variant);
}
private static class YoutubeSortOrderSortFilterItem extends YoutubeSortFilterItem {
private final SortOrder sortOrder;
YoutubeSortOrderSortFilterItem(final int identifier, final String name,
final SortOrder sortOrder) {
super(identifier, name);
this.sortOrder = sortOrder;
}
public SortOrder get() {
return sortOrder;
}
}
private static class YoutubeDateSortFilterItem extends YoutubeSortFilterItem {
private final DateFilter dateFilter;
YoutubeDateSortFilterItem(final int identifier, final String name,
final DateFilter dateFilter) {
super(identifier, name);
this.dateFilter = dateFilter;
}
public DateFilter get() {
return this.dateFilter;
}
}
private static class YoutubeLenSortFilterItem extends YoutubeSortFilterItem {
private final LengthFilter lengthFilter;
YoutubeLenSortFilterItem(final int identifier, final String name,
final LengthFilter lengthFilter) {
super(identifier, name);
this.lengthFilter = lengthFilter;
}
public LengthFilter get() {
return this.lengthFilter;
}
}
private static class YoutubeFeatureSortFilterItem extends YoutubeSortFilterItem {
private final Features feature;
YoutubeFeatureSortFilterItem(final int identifier, final String name,
final Features feature) {
super(identifier, name);
this.feature = feature;
}
public Features get() {
return this.feature;
}
}
public static class YoutubeSortFilterItem extends FilterItem {
public YoutubeSortFilterItem(final int identifier, final String name) {
super(identifier, name);
}
}
public static class YoutubeContentFilterItem extends YoutubeSortFilterItem {
protected String params;
private TypeFilter contentType = null;
public YoutubeContentFilterItem(final int identifier, final String name) {
super(identifier, name);
}
public YoutubeContentFilterItem(final int identifier, final String name,
final TypeFilter contentType) {
super(identifier, name);
this.params = "";
this.contentType = contentType;
}
public String getParams() {
return params;
}
public void setParams(final String params) {
this.params = params;
}
private TypeFilter getContentType() {
return contentType;
}
}
public static class MusicYoutubeContentFilterItem extends YoutubeContentFilterItem {
public MusicYoutubeContentFilterItem(final int identifier, final String name,
final String params) {
super(identifier, name);
this.params = params;
}
}
}

View File

@ -0,0 +1,226 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.services.youtube.search.filter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.DateFilter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.ExtraFeatures;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.Extras;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.Features;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.Filters;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.LengthFilter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.SearchRequest;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.SortOrder;
import org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf.TypeFilter;
import org.schabi.newpipe.extractor.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Base64;
/**
* This class interacts with the auto generated proto buffer java files
* created by the 'squareup.wire proto buffer' plugin.
* <p>
* Below proto buffer description file is used:
* <a href="file:../main/proto/youtube-content-and-sort-filters.proto"
* >youtube-content-and-sort-filters.proto</a>
*/
public final class YoutubeProtoBufferSearchParameterAccessor {
private static final String UTF_8 = "UTF-8";
/**
* the base64 urlencoded sp string
*/
private String searchParameter = "";
private YoutubeProtoBufferSearchParameterAccessor() {
}
@SuppressWarnings("NewApi")
public String encodeSp(final SortOrder sort, final DateFilter date,
final TypeFilter type, final LengthFilter length,
final Features[] features, final ExtraFeatures[] extraFeatures)
throws IOException {
final Filters.Builder filtersBuilder = new Filters.Builder();
if (null != date) {
filtersBuilder.date((long) date.getValue());
}
if (null != type) {
filtersBuilder.type((long) type.getValue());
}
if (null != length) {
filtersBuilder.length((long) length.getValue());
}
if (null != features) {
for (final Features feature : features) {
setFeatureState(feature, true, filtersBuilder);
}
}
final SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
if (null != sort) {
searchRequestBuilder.sorted((long) sort.getValue());
}
if (null != date || null != type || null != length || null != features
// even though extraFeatures is evaluated later. But in the case that extraFeatures
// will be the only activated 'feature' we still need to generate Filters here
// as it is integral part of the SearchRequest object
|| null != extraFeatures) {
final Filters filters = filtersBuilder.build();
searchRequestBuilder.filter(filters);
}
if (null != extraFeatures && extraFeatures.length > 0) {
final Extras.Builder extrasBuilder = new Extras.Builder();
for (final ExtraFeatures extra : extraFeatures) {
setExtraState(extra, true, extrasBuilder);
}
final Extras extras = extrasBuilder.build();
searchRequestBuilder.extras(extras);
}
final SearchRequest searchRequest = searchRequestBuilder.build();
final byte[] protoBufEncoded = searchRequest.encode();
final String protoBufEncodedBase64 = Base64.getEncoder()
.encodeToString(protoBufEncoded);
final String urlEncodedBase64EncodedSearchParameter
= Utils.encodeUrlUtf8(protoBufEncodedBase64);
this.searchParameter = urlEncodedBase64EncodedSearchParameter;
return urlEncodedBase64EncodedSearchParameter;
}
/**
* Decode a sp parameter back to a {@link SearchRequest} object
*
* @param urlEncodedBase64EncodedSearchParameter the parameter says it all
* @return {@link SearchRequest} with decoded search parameter
* @throws IOException
*/
@SuppressWarnings("NewApi")
public SearchRequest decodeSp(final String urlEncodedBase64EncodedSearchParameter)
throws IOException {
final String urlDecodedBase64EncodedSearchParameter
= Utils.decodeUrlUtf8(urlEncodedBase64EncodedSearchParameter);
final byte[] decodedSearchParameter
= Base64.getDecoder().decode(urlDecodedBase64EncodedSearchParameter);
return new SearchRequest.Builder().build().adapter().decode(decodedSearchParameter);
}
public String getSp() {
return this.searchParameter;
}
private void setExtraState(final ExtraFeatures extra,
final boolean enable,
final Extras.Builder extrasBuilder) {
switch (extra) {
case verbatim:
extrasBuilder.verbatim(enable);
break;
}
}
private void setFeatureState(final Features feature,
final boolean enable,
final Filters.Builder filtersBuilder) {
switch (feature) {
case live:
filtersBuilder.live(enable);
break;
case is_4k:
filtersBuilder.is_4k(enable);
break;
case is_hd:
filtersBuilder.is_hd(enable);
break;
case subtitles:
filtersBuilder.subtitles(enable);
break;
case ccommons:
filtersBuilder.ccommons(enable);
break;
case is_360:
filtersBuilder.is_360(enable);
break;
case is_vr180:
filtersBuilder.is_vr180(enable);
break;
case is_3d:
filtersBuilder.is_3d(enable);
break;
case is_hdr:
filtersBuilder.is_hdr(enable);
break;
case location:
filtersBuilder.location(enable);
break;
case purchased:
filtersBuilder.purchased(enable);
break;
}
}
/**
* Build a {@link YoutubeProtoBufferSearchParameterAccessor} Object
*/
public static class Builder {
private final ArrayList<Features> featureList = new ArrayList<>();
private final ArrayList<ExtraFeatures> extraFeatureList = new ArrayList<>();
private final YoutubeProtoBufferSearchParameterAccessor searchParamGenerator =
new YoutubeProtoBufferSearchParameterAccessor();
private SortOrder sort = null;
private DateFilter date = null;
private TypeFilter type = null;
private LengthFilter length = null;
public Builder setSortOrder(final SortOrder sortOrder) {
this.sort = sortOrder;
return this;
}
public Builder setDateFilter(final DateFilter dateFilter) {
this.date = dateFilter;
return this;
}
public Builder setTypeFilter(final TypeFilter typeFilter) {
this.type = typeFilter;
return this;
}
public Builder setLengthFilter(final LengthFilter lengthFilter) {
this.length = lengthFilter;
return this;
}
public Builder addFeature(final Features feature) {
this.featureList.add(feature);
return this;
}
public Builder addExtraFeature(final ExtraFeatures extra) {
this.extraFeatureList.add(extra);
return this;
}
public YoutubeProtoBufferSearchParameterAccessor build() throws IOException {
final Features[] features = this.featureList.toArray(new Features[0]);
final ExtraFeatures[] extraFeat = this.extraFeatureList.toArray(new ExtraFeatures[0]);
this.searchParamGenerator
.encodeSp(this.sort, this.date, this.type, this.length, features, extraFeat);
return this.searchParamGenerator;
}
}
}

View File

@ -0,0 +1,58 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
syntax = "proto2";
package youtubesearchfilter;
option java_multiple_files = true;
option java_package = "org.schabi.newpipe.extractor.services.youtube.search.filter.protobuf";
enum ExtraFeatures {
verbatim = 0;
}
message Extras {
optional bool verbatim = 1;
}
enum Features {
live = 0;
is_4k = 1;
is_hd = 2;
subtitles = 3;
ccommons = 4;
is_360 = 5;
is_vr180 = 6;
is_3d = 7;
is_hdr = 8;
location = 9;
purchased = 10;
}
message Filters {
optional int64 date = 1;
optional int64 type = 2;
optional int64 length = 3;
optional bool is_hd = 4;
optional bool subtitles = 5;
optional bool ccommons = 6;
optional bool is_3d = 7;
optional bool live = 8;
optional bool purchased = 9;
optional bool is_4k = 14;
optional bool is_360 = 15;
optional bool location = 23;
optional bool is_hdr = 25;
optional bool is_vr180 = 26;
}
message SearchRequest {
optional int64 sorted = 1;
optional Filters filter = 2;
optional Extras extras = 8;
}
enum SortOrder { relevance=0; rating=1; date=2; views=3; }
enum DateFilter { hour=1; day=2; week=3; month=4; year=5; }
enum TypeFilter { video=1; channel=2; playlist=3; movie=4; show=5; }
enum LengthFilter { duration_short=1; duration_long=2; duration_medium=3; }