Программа, помогающая искать торренты

Я написал настольное приложение на java (с JavaFX). Программа сначала находит данный заголовок в Imdb через: https://imdb-internet-movie-database-unofficial.p.rapidapi.com а затем, когда пользователь выбирает фильм, поищите на сайте пиратской бухты ссылку на магниты, семена и пиявки.

Вот пример:

введите описание изображения здесь

Вот два класса:

ImdbFinder

package service.movie;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonArray;
import exception.NoMovieFoundException;
import model.Movie;
import service.util.JsonParserUtil;
import service.util.PropertyHelper;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;

public class ImdbFinder implements MovieFinder {

    private static ImdbFinder instance;
    private static Properties properties;
    private static ObjectMapper objectMapper;

    private ImdbFinder() { }

    public static ImdbFinder getInstance() {
        if (instance == null) {
            instance = new ImdbFinder();
            objectMapper = new ObjectMapper();
            properties = PropertyHelper.loadPropertyFileForFilename("ImdbHttpInfo.properties");
        }
        return instance;
    }

    @Override
    public Movie getMovieDetailsForMovieId(String movieId) {
        return new Movie();
    }

    @Override
    public List<Movie> getMovieListForSearchedPhrase(String phrase) throws IOException, InterruptedException, NoMovieFoundException {
        List<Movie> movies = mapTitlesToMovieList(foundMovies(phrase).body());
        if (movies == null || movies.isEmpty()) {
            throw new NoMovieFoundException();
        }
        return movies;
    }

    private List<Movie> mapTitlesToMovieList(String response) throws IOException {
        Optional<JsonArray> jsonArray = Optional.ofNullable(
                JsonParserUtil.parseStringToJsonObject(response).getAsJsonArray("titles"));

        List<Movie> movieList = new ArrayList<>();
        if (jsonArray.isPresent()) {
            movieList = objectMapper.readValue(jsonArray.get().toString(), new TypeReference<List<Movie>>() {});
        }

        return movieList;
    }

    private HttpResponse<String> foundMovies(String title) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(properties.getProperty("search-api-url") + formatTitle(title)))
                .header("x-rapidapi-key", properties.getProperty("x-rapidapi-key"))
                .header("x-rapidapi-host", properties.getProperty("x-rapidapi-host"))
                .method("GET", HttpRequest.BodyPublishers.noBody())
                .build();
        System.out.println(URI.create(properties.getProperty("search-api-url") + formatTitle(title)));
        return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
    }

    private String formatTitle(String title) {
        return title.replaceAll("\s+", "%20")
                .replaceAll("[^a-zA-Z\d\s%]", "");
    }
}

PirateBayFinder

package service.torrent;

import exception.NoTorrentFoundException;
import model.Torrent;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PirateBayFinder implements TorrentFinder {

    private static TorrentFinder instance;

    private static final String SEARCH_URL = "https://tpb.party/search/";

    private static final String MAGNET_REGEX = "magnet:\?xt=(.*?)(?=")";
    private static final String TITLE_INSIDE_MAGNET_REGEX = "(?<=&dn=)(.*?)(?=&tr)";
    private static final String SEEDS_LECHERS_PATTERN = "(?<=<td align="right">)([0-9]*?)(?=</td>)";

    private PirateBayFinder() { }

    public static TorrentFinder getInstance() {
        if (instance == null) {
            instance = new PirateBayFinder();
        }
        return instance;
    }

    @Override
    public List<Torrent> findTorrentsForPhrase(String phrase) throws IOException, InterruptedException, NoTorrentFoundException {

        String htmlPage = foundTorrents(phrase).body();

        List<Torrent> foundTorrents = new ArrayList<>();

        scrapTorrentInfo(htmlPage, foundTorrents);
        scrapSeedsAndLeechers(htmlPage, foundTorrents);

        if (foundTorrents.isEmpty()) {
            throw new NoTorrentFoundException();
        }

        return foundTorrents;
    }

    private void scrapTorrentInfo(String htmlPage, List<Torrent> foundTorrents) {
        Matcher magnetMatcher = Pattern.compile(MAGNET_REGEX).matcher(htmlPage);
        Pattern titlePattern = Pattern.compile(TITLE_INSIDE_MAGNET_REGEX);
        Matcher titleMatcher;
        String title;
        String magnet;
        while (magnetMatcher.find()) {
            magnet = magnetMatcher.group();
            titleMatcher = titlePattern.matcher(magnet);
            if (titleMatcher.find()) {
                title = titleMatcher.group();
                foundTorrents.add(new Torrent(title, magnet));
            }
        }
    }

    private void scrapSeedsAndLeechers(String htmlPage, List<Torrent> foundTorrents) {
        final Matcher seedsLeechersMatcher = Pattern.compile(SEEDS_LECHERS_PATTERN).matcher(htmlPage);
        int seeds, lecheers;
        int counter = 0;
        while(seedsLeechersMatcher.find()) {
            seeds = Integer.parseInt(seedsLeechersMatcher.group());
            lecheers = 0;
            if (seedsLeechersMatcher.find()) {
                lecheers = Integer.parseInt(seedsLeechersMatcher.group());
            }
            foundTorrents.get(counter).setSeeds(seeds);
            foundTorrents.get(counter).setLeechers(lecheers);
            counter++;
        }
    }

    private HttpResponse<String> foundTorrents(String title) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(SEARCH_URL + title.replaceAll("\s+","%20")))
                .method("GET", HttpRequest.BodyPublishers.noBody())
                .build();
        System.out.println(request.uri());
        return HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
    }
}

и два интерфейса:

MovieFinder

public interface MovieFinder {

    Movie getMovieDetailsForMovieId(String id);

    List<Movie> getMovieListForSearchedPhrase(String phrase) throws IOException, InterruptedException;

}

TorrentFinder

public interface TorrentFinder {

    List<Torrent> findTorrentsForPhrase(String phrase) throws IOException, InterruptedException;

}

Подскажите, пожалуйста, что я могу улучшить в этих двух классах? Я считаю, что код, содержащий сопоставители и шаблоны, выглядит слишком сложным.

1 ответ
1

package service.movie;

Имена пакетов обычно связывают код с автором / организацией, например “com.github.mrfisherman.moviefinder” или “com.company.ourapp”.


    public static ImdbFinder getInstance() {
        if (instance == null) {
            instance = new ImdbFinder();
            objectMapper = new ObjectMapper();
            properties = PropertyHelper.loadPropertyFileForFilename("ImdbHttpInfo.properties");
        }
        return instance;
    }

Это не потокобезопасно, при вызове из нескольких потоков может быть создано n экземпляров. Есть три способа исправить это:

  1. Создайте экземпляр в static конструктор. Я бы посоветовал не делать это привычкой, потому что, если ни один экземпляр никогда не понадобится, он все равно может быть создан, если только static доступны поля или методы.
  2. Сделайте весь метод synchronized.
  3. Используйте блокировку с двойной проверкой с volatile поле.
private static volatile ImdbFinder instance;

public static ImdbFinder gestInstance() {
    if (instance == null) {
        synchronized(ImdbFinder.class) {
            if (instance == null) {
                // TODO Init instance here.
            }
        }
    }
    
    return instance;
}

    private static Properties properties;
    private static ObjectMapper objectMapper;

Кажется, это лучше подходит в качестве полей экземпляра.


    @Override
    public Movie getMovieDetailsForMovieId(String movieId) {
        return new Movie();
    }

???


public List<Movie> getMovieListForSearchedPhrase(String phrase) throws IOException, InterruptedException, NoMovieFoundException 

Если метод выдает InterruptedException, что-то пахнет.


    @Override
    public List<Movie> getMovieListForSearchedPhrase(String phrase) throws IOException, InterruptedException, NoMovieFoundException {
        List<Movie> movies = mapTitlesToMovieList(foundMovies(phrase).body());
        if (movies == null || movies.isEmpty()) {
            throw new NoMovieFoundException();
        }
        return movies;
    }

Не сигнализируйте “ничего не найдено” с помощью Exception, вместо этого верните пустой список (не null). Это уменьшит код, использующий эту функцию, от необходимости использовать try...catch блокировать, возможно, даже не делать if.


        Matcher titleMatcher;
        String title;
        String magnet;
        while (magnetMatcher.find()) {
            magnet = magnetMatcher.group();
            titleMatcher = titlePattern.matcher(magnet);
            if (titleMatcher.find()) {
                title = titleMatcher.group();
                foundTorrents.add(new Torrent(title, magnet));
            }
        }

Это не C для какой-то встроенной системы с небольшими ресурсами, это Java. Объявите ли вы переменную вне цикла или нет, не имеет ни малейшего значения, за исключением удобочитаемости и ремонтопригодности. Объявите свои переменные в самом низком возможном объеме.


System.out.println(URI.create(properties.getProperty("search-api-url") + formatTitle(title)));

Не печатайте в стандартный вывод, если этого не ожидаете. Используйте средство ведения журнала, например, используемое по умолчанию в Java, и даже в этом случае старайтесь вести журнал как можно более содержательным. Это больше похоже на отладочную информацию, которая может вам понадобиться в определенных обстоятельствах, чем во время нормальной работы приложения.


public interface MovieFinder {

    Movie getMovieDetailsForMovieId(String id);

    List<Movie> getMovieListForSearchedPhrase(String phrase) throws IOException, InterruptedException;

}

Я всегда советую людям быть как можно более описательными в именах, но это многовато. Movie getMovie(String id) и List<Movie> findMovies(String searchterm) сообщает мне все, что мне нужно знать при просмотре API.

Дополнительно, getMovieListForSearchedPhrase не следует объявлять InterruptedException и IOException (не каждый поисковик может выполнять ввод-вывод и, следовательно, по необходимости), но должен генерировать специализированное исключение, например MovieException или же MovieFinderException, если они действительно нужно что-то бросить. В этом случае оба могут быть в порядке без исключений, так как первый возвращается null если фильма нет, а второй возвращает пустой List если фильмы не найдены. Если вы хотите сообщать о сбоях, которых можно было бы ожидать, используйте одно и то же специализированное исключение для обоих методов.


И последнее, но не менее важное: предупреждение относительно закона и законности. Технически то, что вы здесь делаете, несомненно, хорошо, поскольку вы предоставляете только инструмент. Тем не мение, законно это зависит от юрисдикции, в которой вы находитесь. Инструмент с единственной целью нарушить авторское право во многих частях мира рассматривается так же плохо, как нарушение авторских прав. Даже если не прямо незаконно, существуют группы («представители художников»), которые могут Все еще попробуйте пойти против этого.

Если сомневаетесь, спросите юриста.

  • Большое спасибо за этот CR. Это то, что мне было нужно. По поводу последнего пункта я оставил заметку на github: [Attention!] Я не несу ответственности за неправильное использование или любые правовые последствия использования программы. Я также не призываю вас скачивать фильмы из нелегальных источников. Программа создана с образовательной целью. надеюсь, что на данный момент этого достаточно, потому что я не делюсь незаконными данными и т.

    – MrFisherman

  • «Надеюсь, на данный момент достаточно», как я уже сказал, технически да, юридически, – спросите юриста. Также имейте в виду, что страна хостинг важен в этих вопросах. В целом, то, что также может быть хорошим дизайнерским упражнением, – это сделать его не «MovieFinder», а «TorrentFinder», который обладает способностью сопоставлять различные элементы с торрентами. Таким образом, IMDB становится одной из многих реализаций для другой, чтобы найти дистрибутивы Linux, или заброшенные игры, или общедоступные книги и так далее.

    – Бобби

  • У меня есть еще один вопрос, я знаю, что вы говорите, что я не должен генерировать исключения вне методов, но я сделал это, чтобы поймать их все в контроллере, когда я могу показать пользователю предупреждение gui. Итак, что мне делать сейчас, оставить все как есть и поймать их позже или, как вы сказали, попытаться поймать их тем же методом.

    – MrFisherman


  • Как я уже отмечал, используйте выделенное исключение. Проблема здесь, как я уже сказал, в том, что InterruptedException это странно, а детали реализации. Так бросает IOException. Интерфейс не должен зависеть от поведения реализации, но оба эти исключения являются специфическими для реализации исключениями. Наилучшим способом было бы предоставить общее настраиваемое исключение, как сказано, которое генерируется обоими методами, если вам нужно сигнализировать, что исключение произошло во время этих методов. Это означает, что реализации перехватывают любые конкретные исключения и повторно генерируют их как пользовательские.

    – Бобби

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *