ScheduleApi.java

/*
 * tvmaze-java-client - A client to access the TVMaze API
 * Copyright © 2024-2025 Andy Miles (andy.miles@amilesend.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.amilesend.tvmaze.client.api;

import com.amilesend.client.connection.Connection;
import com.amilesend.client.parse.parser.ListParser;
import com.amilesend.tvmaze.client.model.Episode;
import com.amilesend.tvmaze.client.parse.adapters.LocalDateTypeAdapter;
import okhttp3.HttpUrl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

/**
 * TVMaze API to retrieve schedule information.
 * <br/>
 * For more information, please refer to <a href="https://www.tvmaze.com/api#schedule">
 * https://www.tvmaze.com/api#schedule</a>
 */
public class ScheduleApi extends ApiBase {
    private static final String SCHEDULE_API_PATH = "/schedule";
    private static final String WEB_SCHEDULE_API_PATH = SCHEDULE_API_PATH + "/web";
    private static final int MAX_COUNTRY_CODE_LENGTH = 3;
    private static final Set<String> ISO_COUNTRY_CODES = Set.of(Locale.getISOCountries());

    /**
     * Creates a new {@code ScheduleApi} object
     *
     * @param connection the connection
     */
    public ScheduleApi(final Connection connection) {
        super(connection);
    }

    ////////////////
    // getSchedule
    ////////////////

    /**
     * Retrieves the list of episodes that air in a given country and date.
     *
     * @param countryCode the ISO 3166-1 country code (e.g., "US"), or {@code null}
     * @param date the date, or {@code null} for the current date
     * @return the list of airing episodes
     */
    public List<Episode> getSchedule(final String countryCode, final LocalDate date) {
        final HttpUrl url = formatScheduleUrl(SCHEDULE_API_PATH, countryCode, date);
        return connection.execute(
                connection.newRequestBuilder()
                        .url(url)
                        .build(),
                new ListParser<>(Episode.class));
    }

    ////////////////////////////
    // getWebStreamingSchedule
    ////////////////////////////

    /**
     * Retrieves the list of episodes that air on web/streaming channels in a given country and date.
     *
     * @param countryCode the ISO 3166-1 country code (e.g., "US"), or {@code null}
     * @param date the date, or {@code null} for the current date
     * @return the list of airing episodes
     */
    public List<Episode> getWebStreamingSchedule(final String countryCode, final LocalDate date) {
        final HttpUrl url = formatScheduleUrl(WEB_SCHEDULE_API_PATH, countryCode, date);
        return connection.execute(
                connection.newRequestBuilder()
                        .url(url)
                        .build(),
                new ListParser<>(Episode.class));
    }

    ////////////////////
    // getFullSchedule
    ////////////////////

    /**
     * Retrieves the list of all future episodes. Note: This operation is expensive.
     *
     * @return the list of future episodes
     */
    public List<Episode> getFullSchedule() {
        return connection.execute(
                connection.newRequestBuilder()
                        .url(getFullScheduleUrl())
                        .build(),
                new ListParser<>(Episode.class));
    }

    private HttpUrl getFullScheduleUrl() {
        return HttpUrl.parse(
                new StringBuilder(connection.getBaseUrl())
                        .append("/schedule/full")
                        .toString());
    }

    private HttpUrl formatScheduleUrl(final String apiPath, final String countryCode, final LocalDate date) {
        Validate.notBlank(apiPath, "apiPath must not be blank");

        final HttpUrl.Builder urlBuilder = HttpUrl.parse(
                new StringBuilder(connection.getBaseUrl())
                        .append(apiPath)
                        .toString())
                .newBuilder();
        if (StringUtils.isNotBlank(countryCode)) {
            urlBuilder.addQueryParameter("country", validateAndFormatCountryCode(countryCode));
        }
        Optional.ofNullable(date)
                .ifPresent(d -> urlBuilder.addQueryParameter("date", d.format(LocalDateTypeAdapter.FORMATTER)));

        return urlBuilder.build();
    }

    private static String validateAndFormatCountryCode(final String countryCode) {
        Validate.notBlank(countryCode, "countryCode must not be blank");
        Validate.isTrue(countryCode.length() <= MAX_COUNTRY_CODE_LENGTH,
                "countryCode length must be <= " + MAX_COUNTRY_CODE_LENGTH);
        Validate.isTrue(ISO_COUNTRY_CODES.contains(countryCode),
                "countryCode must be a valid ISO 3166-1 country code");

        return URLEncoder.encode(countryCode, StandardCharsets.UTF_8);
    }
}