searchfilters: a framework to create sort and content filter objects

FilterContainer.java:
=====================
This class is a container that keeps either content filters or sort filters organized.
Sort/content filters ({@link FilterItem}s) are organized within {@link FilterGroup}s.

FilterGroup.java:
=================
This class represents a filter category/group. For example 'Sort order'.

Its main purpose is to host a bunch of {@link FilterItem}s that belong to that
group. Eg. 'Relevance', 'Views', 'Rating'

FilterItem.java:
================
This class represents a single filter option.
*More in detail:*
For example youtube offers the filter group 'Sort order'. This group
consists of filter options like 'Relevance', 'Views', 'Rating' etc.
-> for each filter option a FilterItem has to be created.

BaseSearchFilters.java:
=======================
The base class for every service describing their {@link FilterItem}s,
{@link FilterGroup}s, the relation between content filters and sort filters.
This commit is contained in:
evermind 2022-08-19 00:29:36 +02:00 committed by Stypox
parent 9ab932e394
commit 3398a54908
No known key found for this signature in database
GPG Key ID: 4BDF1B40A49FDD23
4 changed files with 452 additions and 0 deletions

View File

@ -0,0 +1,162 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.search.filter;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* The base class for every service describing their {@link FilterItem}s,
* {@link FilterGroup}s, the relation between content filters and sort filters.
*/
public abstract class BaseSearchFilters {
protected final Map<Integer, FilterContainer> sortFilterVariants = new HashMap<>();
protected FilterGroup.Factory groupsFactory = new FilterGroup.Factory();
protected List<FilterItem> selectedContentFilter = null;
protected List<FilterItem> selectedSortFilter;
protected FilterContainer contentFiltersVariant;
protected List<FilterGroup> contentFilterGroups = new LinkedList<>();
protected BaseSearchFilters() {
init();
build();
}
/**
* Set the user selected sort filters which the user has selected in the UI.
*
* @param selectedSortFilter list with sort filters identifiers
*/
public void setSelectedSortFilter(final List<FilterItem> selectedSortFilter) {
this.selectedSortFilter = selectedSortFilter;
}
/**
* Set the selected content filter
*
* @param selectedContentFilter the name of the content filter
*/
public void setSelectedContentFilter(final List<FilterItem> selectedContentFilter) {
this.selectedContentFilter = selectedContentFilter;
}
/**
* Evaluate content and sort filters. This method should be run after:
* {@link #setSelectedContentFilter(List)} and {@link #setSelectedSortFilter(List)}
* <p>
* Note: Whether you should implement this method or {@link #evaluateSelectedContentFilters()}
* and/or {@link #evaluateSelectedSortFilters()} depends on your service needs and/or
* how you want to implement.
*
* @return the query that should be appended to the searchUrl/whatever
*/
public String evaluateSelectedFilters(final String searchString) {
// please implement method in derived class if you want to use it
return null;
}
/**
* Evaluate content filters. This method should be run after:
* {@link #setSelectedContentFilter(List)}
*
* @return the sortQuery that should be appended to the searchUrl/whatever
*/
public String evaluateSelectedContentFilters() {
// please implement method in derived class if you want to use it
return null;
}
/**
* Evaluate sort filters. This method should be run after:
* {@link #setSelectedSortFilter(List)}
*
* @return the contentQuery that should be appended to the searchUrl/whatever
*/
public String evaluateSelectedSortFilters() {
// please implement method in derived class if you want to use it
return null;
}
/**
* create all 'sort' and 'content filter' items and all 'sort filter variants' in this method.
* See eg. {@link YoutubeFilters#init()}
*/
protected abstract void init();
/**
* Transform the filter group list into an array and create the {@link FilterContainer}
* with the content filters that are present for this service (eg. YouTube).
*/
protected void build() {
if (contentFilterGroups == null) {
throw new RuntimeException("Never call method build() twice");
}
this.contentFiltersVariant = new FilterContainer(
contentFilterGroups.toArray(new FilterGroup[0]));
// building done
contentFilterGroups.clear();
contentFilterGroups = null;
}
/**
* Add content Filter SortVariants.
* <p>
* Each content filter may have a corresponding sort filter variant.
*
* @param contentFilterId the content filter this sort variant applies to
* @param variant the corresponding sort filter variant
*/
protected void addContentFilterSortVariant(
final int contentFilterId,
final FilterContainer variant) {
this.sortFilterVariants.put(contentFilterId, variant);
}
/**
* Get (if available) the sort filter variant for this content filter id.
*
* @param identifier the id of a content {@link FilterItem}
* @return the sort filter variant for above stated content filter item. Null if there is none.
*/
public FilterContainer getContentFilterSortFilterVariant(
final int identifier) {
return this.sortFilterVariants.get(identifier);
}
/**
* Get all available content filters for this service.
*
* @return all available content filters
*/
public FilterContainer getContentFilters() {
return this.contentFiltersVariant;
}
/**
* Get the {@link FilterItem} for the corresponding Id.
*
* @param filterId the filter id
* @return the corresponding filter, null if none exists
*/
public FilterItem getFilterItem(final int filterId) {
return groupsFactory.getFilterForId(filterId);
}
/**
* Add the content filter groups that should be available
*/
protected void addContentFilterGroup(final FilterGroup filterGroup) {
if (contentFilterGroups != null) {
contentFilterGroups.add(filterGroup);
} else {
throw new RuntimeException("Never call this method after build()");
}
}
}

View File

@ -0,0 +1,45 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.search.filter;
import java.util.HashMap;
import java.util.Map;
/**
* This class is a container that keeps either content filters or sort filters organized.
*
* Sort/content filters ({@link FilterItem}s) are organized within {@link FilterGroup}s.
*/
public final class FilterContainer {
/**
* Mark {@link FilterItem}'s and {@link FilterGroup}'s which identifier is not (yet) set.
*/
public static final int ITEM_IDENTIFIER_UNKNOWN = -1;
private final Map<Integer, FilterItem> idToFilterItem = new HashMap<>();
private final FilterGroup[] filterGroups;
public FilterContainer(final FilterGroup[] filterGroups) {
this.filterGroups = filterGroups;
for (final FilterGroup group : filterGroups) {
for (final FilterItem item : group.getFilterItems()) {
idToFilterItem.put(item.getIdentifier(), item);
}
}
}
/**
* Quickly access a {@link FilterItem} that belongs to this {@link FilterContainer}.
*
* @param id the identifier of the {@link FilterItem}
* @return
*/
public FilterItem getFilterItem(final int id) {
return idToFilterItem.get(id);
}
public FilterGroup[] getFilterGroups() {
return filterGroups;
}
}

View File

@ -0,0 +1,186 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.search.filter;
import java.util.HashMap;
import java.util.Map;
/**
* This class represents a filter category/group. For example 'Sort order'.
* <p>
* Its main purpose is to host a bunch of {@link FilterItem}s that belong to that
* group. Eg. 'Relevance', 'Views', 'Rating'
*/
public final class FilterGroup {
/**
* {@link #getIdentifier()}
*/
private final int identifier;
/**
* The name of the filter group that the user will see
*/
private final String groupName;
/**
* Specify whether only one item can be selected in this group at a time.
*/
private final boolean onlyOneCheckable;
/**
* Each group may have a default value that should be selected.
* <p>
* It should be set to the the {@link FilterItem}'s id. If there is no default option
* it should be set to {@link FilterContainer#ITEM_IDENTIFIER_UNKNOWN}
*/
private final int defaultSelectedFilterId;
/**
* The filter items that belong to this {@link FilterGroup}.
*/
private final FilterItem[] filterItems;
/**
* {@link #getAllSortFilters()}.
*/
private final FilterContainer allSortFilters;
private FilterGroup(final int identifier,
final String groupName,
final boolean onlyOneCheckable,
final int defaultSelectedFilterId,
final FilterItem[] filterItems,
final FilterContainer allSortFilters) {
this.identifier = identifier;
this.groupName = groupName;
this.onlyOneCheckable = onlyOneCheckable;
this.defaultSelectedFilterId = defaultSelectedFilterId;
this.filterItems = filterItems;
this.allSortFilters = allSortFilters;
}
/**
* If this group is a content filter and has corresponding sort filters, this
* {@link FilterContainer} contains all available sort filters for this group.
*
* @return may be null as not all {@link FilterGroup}s have sort filters.
*/
public FilterContainer getAllSortFilters() {
return allSortFilters;
}
/**
* {@link FilterItem#getIdentifier()}
*/
public int getIdentifier() {
return this.identifier;
}
/**
* {@link #groupName}
*/
public String getName() {
return groupName;
}
/**
* {@link #defaultSelectedFilterId}
*/
public int getDefaultSelectedFilterId() {
return defaultSelectedFilterId;
}
/**
* {@link #filterItems}
*/
public FilterItem[] getFilterItems() {
return filterItems;
}
/**
* {@link #onlyOneCheckable}
*/
public boolean isOnlyOneCheckable() {
return onlyOneCheckable;
}
/**
* Factory for building {@link FilterGroup}s.
* <p>
* Each service should only have one instance.
* This is implemented in {@link BaseSearchFilters}
*/
public static class Factory {
/**
* A map that has all {@link FilterItem}s that are relevant for one service. Eg. Youtube
*/
public final Map<Integer, FilterItem> filtersMap = new HashMap<>();
/**
* Check if a {@link FilterItem} has a unique id.
*
* @param filterItems a map with the previously added {@link FilterItem}'s to compare with.
* @param item the new {@link FilterItem} that should be added.
*/
void uniqueIdChecker(final Map<Integer, FilterItem> filterItems,
final FilterItem item) {
if (item.getIdentifier() == FilterContainer.ITEM_IDENTIFIER_UNKNOWN
&& !(item instanceof FilterItem.DividerItem)) {
throw new InvalidFilterIdException("Filter ID "
+ item.getIdentifier() + " aka FilterContainer.ITEM_IDENTIFIER_UNKNOWN"
+ " for \"" + item.getName() + "\" not allowed");
}
if (filterItems.containsKey(item.getIdentifier())) {
final FilterItem storedItem = filterItems.get(item.getIdentifier());
throw new InvalidFilterIdException("Filter ID "
+ item.getIdentifier() + " for \"" + item.getName()
+ "\" already taken from \"" + storedItem.getName() + "\"");
}
}
/**
* Add a new {@link FilterItem} that is relevant to this service.
* <p>
* The {@link FilterItem}s are accessible by their id via {@link #getFilterForId(int)}
*
* @param filter the new {@link FilterItem} to be added to the factory.
* @return the identifier of the {@link FilterItem}
*/
public int addFilterItem(final FilterItem filter) {
uniqueIdChecker(filtersMap, filter);
filtersMap.put(filter.getIdentifier(), filter);
return filter.getIdentifier();
}
public FilterGroup createFilterGroup(final int identifier,
final String groupName,
final boolean onlyOneCheckable,
final int defaultSelectedFilterId,
final FilterItem[] filterItems,
final FilterContainer allSortFilters) {
return new FilterGroup(identifier, groupName, onlyOneCheckable,
defaultSelectedFilterId, filterItems, allSortFilters);
}
/**
* Get previously via {@link #addFilterItem(FilterItem)} added {@link FilterItem}.
*
* @param identifier the id of the desired {@link FilterItem}
* @return the desired {@link FilterItem}
*/
public FilterItem getFilterForId(final int identifier) {
return filtersMap.get(identifier);
}
private static class InvalidFilterIdException extends RuntimeException {
InvalidFilterIdException(final String message) {
super(message);
}
}
}
}

View File

@ -0,0 +1,59 @@
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
package org.schabi.newpipe.extractor.search.filter;
/**
* This class represents a single filter option.
* <p>
* <b>More in detail:</b>
* For example youtube offers the filter group 'Sort order'. This group
* consists of filter options like 'Relevance', 'Views', 'Rating' etc.
* -> for each filter option a FilterItem has to be created.
*/
public class FilterItem {
/**
* The name of the filter option, that will be visible to the user.
*/
private final String name;
/**
* A sequential unique number identifier.
*
* <b>Note:</b>
* - the uniqueness applies only to each service.
* - Never reuse a previously unique number for another filter option/group
* (Otherwise implementation in the client that may implement to store some user
* specified defaults could have an undefined behaviour while loading).
*/
private final int identifier;
public FilterItem(final int identifier, final String name) {
this.identifier = identifier;
this.name = name;
}
/**
* @return {@link #identifier}
*/
public int getIdentifier() {
return this.identifier;
}
/**
* @return {@link #name}
*/
public String getName() {
return this.name;
}
/**
* This class is used to have a sub title divider between regular {@link FilterItem}s.
*/
public static class DividerItem extends FilterItem {
public DividerItem(final String name) {
super(FilterContainer.ITEM_IDENTIFIER_UNKNOWN, name);
}
}
}