├── .gitignore ├── README.MD ├── build.sbt └── src └── main └── java └── de └── kaysubs └── tracker └── nyaasi ├── NyaaSiApi.java ├── NyaaSiApiImpl.java ├── NyaaSiAuthApi.java ├── NyaaSiAuthApiImpl.java ├── examples ├── AccountInfoExample.java ├── ChangeEmailExample.java ├── ChangePasswordExample.java ├── CommentExamples.java ├── LoginExample.java ├── SearchExamples.java ├── TorrentInfoExample.java └── UploadExample.java ├── exception ├── CannotEditException.java ├── CaptchaException.java ├── IllegalCategoryException.java ├── LoginException.java ├── MissingTrackerException.java ├── NoSuchCommentException.java ├── NoSuchTorrentException.java ├── NyaaSiException.java ├── PasswordLengthException.java ├── PermissionException.java └── WebScrapeException.java ├── model ├── AccountInfo.java ├── Category.java ├── DataSize.java ├── EditTorrentRequest.java ├── MainCategory.java ├── SearchRequest.java ├── Session.java ├── SubCategory.java ├── TorrentInfo.java ├── TorrentPreview.java ├── TorrentState.java └── UploadTorrentRequest.java ├── util └── SearchIterator.java └── webscrape ├── AccountInfoCsrfTokenParser.java ├── AccountInfoParser.java ├── DeleteCsrfTokenParser.java ├── EditCommentCsrfTokenParser.java ├── EditTorrentParser.java ├── LoginCsrfTokenParser.java ├── ParseUtils.java ├── Parser.java ├── TorrentInfoParser.java ├── TorrentListPage.java ├── UploadCsrfTokenParser.java ├── ValidateDeleteComment.java ├── ValidateEmailChange.java ├── ValidatePasswordChange.java ├── ValidateUploadResponse.java ├── WriteCommentCsrfTokenParser.java └── WriteCommentResponseParser.java /.gitignore: -------------------------------------------------------------------------------- 1 | /project 2 | /target 3 | *.class 4 | hs_err_* 5 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # NyaaSi-API 2 | Java API implementation for https://nyaa.si/ and https://sukebei.nyaa.si/ 3 | 4 | Since this api parses webpages, it might break any time. 5 | 6 | # Installation 7 | One way to use this library is to inculde it through jitpack. 8 | How to use it with your build tool is explained at [their page](https://jitpack.io/#aki-ks/NyaaSi-API/master). 9 | 10 | If you're building with sbt, you may add this library as a remote project in your `build.sbt` and let your project depend on it. 11 | Sbt builds the library from source and will apply all new commits to the repository. 12 | ``` sbt 13 | lazy val nyaaSi = RootProject(uri("https://github.com/kaysubs/NyaaSi-API")) 14 | dependsOn(nyaaSi) 15 | ``` 16 | 17 | # Usage 18 | The entry point of this API is the `NyaaSiApi.getNyaa` method for access to https://nyaa.si/ and `NyaaSiApi.getSukebei` for https://sukebei.nyaa.si/ 19 | 20 | For more example usages you may have a look at the `de.kaysubs.tracker.nyaasi.examples` package. 21 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "de.kaysubs" 2 | name := "NyaaSi-API" 3 | version := "0.1-SNAPSHOT" 4 | 5 | description := "Java API for nyaa.si and sukebei.nyaa.si" 6 | 7 | javacOptions in (Compile, compile) ++= Seq("-source", "1.8", "-target", "1.8") 8 | 9 | crossPaths := false // drop off Scala suffix from artifact names. 10 | autoScalaLibrary := false // exclude scala-library from dependencies 11 | 12 | val httpClientVersion = "4.5.5" 13 | val jsoupVersion = "1.11.2" 14 | lazy val kaysubsCommons = RootProject(uri("https://github.com/kaysubs/kaysub-commons.git")) 15 | 16 | dependsOn(kaysubsCommons) 17 | 18 | libraryDependencies += "org.apache.httpcomponents" % "httpclient" % httpClientVersion 19 | libraryDependencies += "org.apache.httpcomponents" % "httpmime" % httpClientVersion 20 | libraryDependencies += "org.jsoup" % "jsoup" % jsoupVersion 21 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/NyaaSiApi.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi; 2 | 3 | import de.kaysubs.tracker.common.exception.HttpException; 4 | import de.kaysubs.tracker.nyaasi.exception.IllegalCategoryException; 5 | import de.kaysubs.tracker.nyaasi.exception.LoginException; 6 | import de.kaysubs.tracker.nyaasi.exception.NoSuchTorrentException; 7 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 8 | import de.kaysubs.tracker.nyaasi.model.SearchRequest; 9 | import de.kaysubs.tracker.nyaasi.model.TorrentInfo; 10 | import de.kaysubs.tracker.nyaasi.model.TorrentPreview; 11 | 12 | public interface NyaaSiApi { 13 | /** 14 | * API for https://sukebei.nyaa.si/ 15 | */ 16 | static NyaaSiApi getSukebei() { 17 | return NyaaSiApiImpl.getSukebeiInstance(); 18 | } 19 | 20 | /** 21 | * API for http://nyaa.si/ 22 | */ 23 | static NyaaSiApi getNyaa() { 24 | return NyaaSiApiImpl.getNyaaInstance(); 25 | } 26 | 27 | boolean isSukebei(); 28 | 29 | /** 30 | * Search for torrents 31 | * 32 | * Since this api call is based on parsing webpages, it might break anytime. 33 | * 34 | * @throws IllegalCategoryException cannot use sukebei categories on nyaa and the other way round 35 | * @throws WebScrapeException error while parsing webpage 36 | * @throws HttpException networking error 37 | */ 38 | TorrentPreview[] search(SearchRequest request); 39 | 40 | /** 41 | * Get informations about a torrent 42 | * 43 | * Since this api call is based on parsing webpages, it might break anytime. 44 | * 45 | * @throws NoSuchTorrentException torrent id does not exist 46 | * @throws WebScrapeException error while parsing webpage 47 | * @throws HttpException networking error 48 | */ 49 | TorrentInfo getTorrentInfo(int torrentId); 50 | 51 | /** 52 | * Login with username and password. 53 | * 54 | * Since this api call is based on parsing webpages, it might break anytime. 55 | * 56 | * @throws LoginException login failed 57 | * @throws WebScrapeException error while parsing webpage 58 | * @throws HttpException networking error 59 | */ 60 | NyaaSiAuthApi login(String username, String password); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/NyaaSiApiImpl.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi; 2 | 3 | import de.kaysubs.tracker.common.HttpUtil; 4 | import de.kaysubs.tracker.common.exception.HttpErrorCodeException; 5 | import de.kaysubs.tracker.common.exception.HttpException; 6 | import de.kaysubs.tracker.nyaasi.exception.*; 7 | import de.kaysubs.tracker.nyaasi.model.SearchRequest; 8 | import de.kaysubs.tracker.nyaasi.model.Session; 9 | import de.kaysubs.tracker.nyaasi.model.TorrentInfo; 10 | import de.kaysubs.tracker.nyaasi.model.TorrentPreview; 11 | import de.kaysubs.tracker.nyaasi.webscrape.*; 12 | import org.apache.http.Consts; 13 | import org.apache.http.HttpResponse; 14 | import org.apache.http.NameValuePair; 15 | import org.apache.http.client.CookieStore; 16 | import org.apache.http.client.entity.UrlEncodedFormEntity; 17 | import org.apache.http.client.methods.HttpGet; 18 | import org.apache.http.client.methods.HttpPost; 19 | import org.apache.http.client.utils.URIBuilder; 20 | import org.apache.http.cookie.Cookie; 21 | import org.apache.http.impl.client.BasicCookieStore; 22 | import org.apache.http.message.BasicNameValuePair; 23 | import org.jsoup.Jsoup; 24 | import org.jsoup.nodes.Document; 25 | 26 | import java.net.URI; 27 | import java.net.URISyntaxException; 28 | import java.util.ArrayList; 29 | import java.util.Arrays; 30 | import java.util.List; 31 | 32 | public class NyaaSiApiImpl implements NyaaSiApi { 33 | private final static NyaaSiApiImpl SUKEBEI_INSTANCE = new NyaaSiApiImpl(true); 34 | private final static NyaaSiApiImpl NYAA_INSTANCE = new NyaaSiApiImpl(false); 35 | 36 | public static NyaaSiApiImpl getSukebeiInstance() { 37 | return SUKEBEI_INSTANCE; 38 | } 39 | 40 | public static NyaaSiApiImpl getNyaaInstance() { 41 | return NYAA_INSTANCE; 42 | } 43 | 44 | protected final boolean isSukebei; 45 | protected final String domain; 46 | 47 | public NyaaSiApiImpl(boolean isSukebei) { 48 | this.isSukebei = isSukebei; 49 | this.domain = isSukebei ? "sukebei.nyaa.si" : "nyaa.si"; 50 | } 51 | 52 | @Override 53 | public boolean isSukebei() { 54 | return isSukebei; 55 | } 56 | 57 | protected T parsePage(HttpResponse response, Parser parser) { 58 | Document page = Jsoup.parse(HttpUtil.readIntoString(response)); 59 | 60 | try { 61 | return parser.parsePage(page, isSukebei); 62 | } catch(NyaaSiException | HttpException e) { 63 | throw e; 64 | } catch(Exception e) { 65 | throw new WebScrapeException(e); 66 | } 67 | } 68 | 69 | @Override 70 | public TorrentPreview[] search(SearchRequest request) { 71 | URI uri; 72 | try { 73 | URIBuilder builder = new URIBuilder() 74 | .setScheme("https") 75 | .setHost(domain); 76 | 77 | request.getTerm().ifPresent(term -> 78 | builder.addParameter("q", term)); 79 | 80 | request.getCategory().ifPresent(category -> { 81 | if(category.isSukebei() != isSukebei) 82 | throw new IllegalCategoryException(); 83 | 84 | builder.addParameter("c", category.getMainCategoryId() + "_" + category.getSubCategoryId()); 85 | }); 86 | 87 | request.getFilter().ifPresent(filter -> 88 | builder.addParameter("f", Integer.toString(filter.getId()))); 89 | 90 | request.getUser().ifPresent(user -> 91 | builder.addParameter("u", user)); 92 | 93 | request.getPage().ifPresent(page -> 94 | builder.addParameter("p", Integer.toString(page))); 95 | 96 | request.getOrdering().ifPresent(ordering -> 97 | builder.addParameter("o", ordering.getId())); 98 | 99 | request.getSortedBy().ifPresent(sort -> 100 | builder.addParameter("s", sort.getId())); 101 | 102 | uri = builder.build(); 103 | } catch (URISyntaxException e) { 104 | throw new HttpException("Cannot build URL", e); 105 | } 106 | 107 | HttpGet get = new HttpGet(uri); 108 | get.setConfig(HttpUtil.WITH_TIMEOUT); 109 | 110 | HttpResponse response = HttpUtil.executeRequest(get); 111 | int statusCode = response.getStatusLine().getStatusCode(); 112 | switch(statusCode) { 113 | case 404: return new TorrentPreview[0]; 114 | case 200: return parsePage(response, new TorrentListPage()); 115 | default: throw new HttpErrorCodeException(statusCode); 116 | } 117 | } 118 | 119 | @Override 120 | public TorrentInfo getTorrentInfo(int torrentId) { 121 | HttpGet get = new HttpGet("https://" + domain + "/view/" + torrentId); 122 | get.setConfig(HttpUtil.WITH_TIMEOUT); 123 | 124 | HttpResponse response = HttpUtil.executeRequest(get); 125 | 126 | if(response.getStatusLine().getStatusCode() == 404) 127 | throw new NoSuchTorrentException(torrentId); 128 | 129 | return parsePage(response, new TorrentInfoParser()); 130 | } 131 | 132 | private String newLoginCsrfToken(CookieStore store) { 133 | HttpGet get = new HttpGet("https://" + domain + "/login"); 134 | get.setConfig(HttpUtil.WITH_TIMEOUT); 135 | 136 | HttpResponse response = HttpUtil.executeRequest(get, store); 137 | return parsePage(response, new LoginCsrfTokenParser()); 138 | } 139 | 140 | @Override 141 | public NyaaSiAuthApi login(String username, String password) { 142 | CookieStore store = new BasicCookieStore(); 143 | String csrfToken = newLoginCsrfToken(store); 144 | 145 | HttpPost post = new HttpPost("https://" + domain + "/login"); 146 | post.setConfig(HttpUtil.WITH_TIMEOUT); 147 | 148 | List form = new ArrayList<>(); 149 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 150 | form.add(new BasicNameValuePair("username", username)); 151 | form.add(new BasicNameValuePair("password", password)); 152 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 153 | 154 | HttpResponse response = HttpUtil.executeRequest(post, store); 155 | boolean didFail = Arrays.stream(response.getHeaders("Location")) 156 | .anyMatch(e -> e.getValue().endsWith("/login")); 157 | 158 | if(didFail) { 159 | throw new LoginException(); 160 | } else { 161 | Session session = sessionFromCookies(store.getCookies()); 162 | return new NyaaSiAuthApiImpl(session, isSukebei); 163 | } 164 | } 165 | 166 | private Session sessionFromCookies(List cookies) { 167 | String sessionId = cookies.stream() 168 | .filter(c -> c.getName().equals("session")).findFirst() 169 | .map(Cookie::getValue) 170 | .orElseThrow(() -> new HttpException("Server did not respond with a session cookie")); 171 | 172 | return new Session(sessionId, isSukebei); 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/NyaaSiAuthApi.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi; 2 | 3 | import de.kaysubs.tracker.common.exception.HttpException; 4 | import de.kaysubs.tracker.nyaasi.exception.*; 5 | import de.kaysubs.tracker.nyaasi.model.AccountInfo; 6 | import de.kaysubs.tracker.nyaasi.model.EditTorrentRequest; 7 | import de.kaysubs.tracker.nyaasi.model.UploadTorrentRequest; 8 | 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | * API calls that require authentication. 13 | */ 14 | public interface NyaaSiAuthApi extends NyaaSiApi { 15 | 16 | /** 17 | * Get information about your account. 18 | * 19 | * Since this api call is based on parsing webpages, it might break anytime. 20 | * 21 | * @throws WebScrapeException error while parsing webpage 22 | * @throws HttpException networking error 23 | */ 24 | AccountInfo getAccountInfo(); 25 | 26 | /** 27 | * Change your email address 28 | * 29 | * Since this api call is based on parsing webpages, it might break anytime. 30 | * 31 | * @throws LoginException wrong password 32 | * @throws WebScrapeException error while parsing webpage 33 | * @throws HttpException networking error 34 | */ 35 | void changeEmail(String currentPassword, String newEmail); 36 | 37 | /** 38 | * Change your password. 39 | * 40 | * Since this api call is based on parsing webpages, it might break anytime. 41 | * 42 | * @throws LoginException wrong password 43 | * @throws WebScrapeException error while parsing webpage 44 | * @throws HttpException networking error 45 | */ 46 | void changePassword(String currentPassword, String newPassword); 47 | 48 | /** 49 | * Upload a torrent. 50 | * 51 | * If you've just registered at nyaa.si, 52 | * you must solve a captcha and cannot use this api yet. 53 | * After ~1 week the captcha should disappear. 54 | * 55 | * The seedfile must contain the tracker 56 | * http://nyaa.tracker.wf:7777/announce when uploading to nyaa or 57 | * http://sukebei.tracker.wf:8888/announce when uploading to sukebei. 58 | * 59 | * Since this api call is based on parsing webpages, it might break anytime. 60 | * 61 | * @return torrent id 62 | * 63 | * @throws CaptchaException you must solve a captcha before upload 64 | * @throws MissingTrackerException torrent does not contain the required tracker 65 | * @throws IllegalCategoryException cannot use sukebei categories on nyaa and the other way round 66 | * @throws WebScrapeException error while parsing webpage 67 | * @throws HttpException networking error 68 | */ 69 | int uploadTorrent(UploadTorrentRequest request); 70 | 71 | /** 72 | * Delete a torrent uploaded with this account. 73 | * 74 | * Since this api call is based on parsing webpages, it might break anytime. 75 | * 76 | * @throws PermissionException you are not allowed to delete this torrent 77 | * @throws NoSuchTorrentException the torrent does not exist 78 | * @throws WebScrapeException error while parsing webpage 79 | * @throws HttpException networking error 80 | */ 81 | void deleteTorrent(int torrentId); 82 | 83 | /** 84 | * Edit a torrent uploaded with this account. 85 | * 86 | * Since this api call is based on parsing webpages, it might break anytime. 87 | * 88 | * @throws IllegalCategoryException cannot use sukebei categories on nyaa and the other way round 89 | * @throws PermissionException you are not allowed to edit this torrent 90 | * @throws NoSuchTorrentException the torrent does not exist 91 | * @throws WebScrapeException error while parsing webpage 92 | * @throws HttpException networking error 93 | */ 94 | void editTorrent(int torrentId, Consumer f); 95 | 96 | /** 97 | * Post a comment below a torrent. 98 | * 99 | * If you've just registered at nyaa.si you must solve a captcha 100 | * before writing comments and cannot use this api call yet. 101 | * After ~1 week the captcha should disappear. 102 | * 103 | * Since this api call is based on parsing webpages, it might break anytime. 104 | * 105 | * @return comment id 106 | * @throws CaptchaException you must solve a captcha 107 | * @throws NoSuchTorrentException the torrent does not exist 108 | * @throws WebScrapeException error while parsing webpage 109 | * @throws HttpException networking error 110 | */ 111 | int writeComment(int torrentId, String message); 112 | 113 | /** 114 | * Edit a comment below a torrent. 115 | * 116 | * Comments can only be edited within one hour after they've posted. 117 | * 118 | * Since this api call is based on parsing webpages, it might break anytime. 119 | * 120 | * @throws NoSuchCommentException comment does not exist 121 | * @throws NoSuchTorrentException the torrent does not exist 122 | * @throws CannotEditException torrent cannot be edited 123 | * @throws WebScrapeException error while parsing webpage 124 | * @throws HttpException networking error 125 | */ 126 | void editComment(int torrentId, int commentId, String newMessage); 127 | 128 | /** 129 | * Delete a comment below a torrent. 130 | * 131 | * Since this api call is based on parsing webpages, it might break anytime. 132 | * 133 | * @throws NoSuchCommentException comment does not exist 134 | * @throws PermissionException tried to delete comment from other user 135 | * @throws WebScrapeException error while parsing webpage 136 | * @throws HttpException networking error 137 | */ 138 | void deleteComment(int torrentId, int commentId); 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/NyaaSiAuthApiImpl.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi; 2 | 3 | import de.kaysubs.tracker.common.HttpUtil; 4 | import de.kaysubs.tracker.common.exception.HttpErrorCodeException; 5 | import de.kaysubs.tracker.nyaasi.exception.*; 6 | import de.kaysubs.tracker.nyaasi.model.*; 7 | import de.kaysubs.tracker.nyaasi.webscrape.*; 8 | import org.apache.http.Consts; 9 | import org.apache.http.HttpResponse; 10 | import org.apache.http.NameValuePair; 11 | import org.apache.http.client.CookieStore; 12 | import org.apache.http.client.entity.UrlEncodedFormEntity; 13 | import org.apache.http.client.methods.HttpGet; 14 | import org.apache.http.client.methods.HttpPost; 15 | import org.apache.http.cookie.Cookie; 16 | import org.apache.http.entity.ContentType; 17 | import org.apache.http.entity.mime.MultipartEntityBuilder; 18 | import org.apache.http.impl.client.BasicCookieStore; 19 | import org.apache.http.message.BasicNameValuePair; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.function.Consumer; 24 | import java.util.regex.Matcher; 25 | import java.util.regex.Pattern; 26 | 27 | public class NyaaSiAuthApiImpl extends NyaaSiApiImpl implements NyaaSiAuthApi { 28 | private final static Pattern VIEW_URL_PATTERN = Pattern.compile("https?://(?:sukebei\\.)?nyaa.si/view/([0-9]+)"); 29 | 30 | private final Session session; 31 | 32 | public NyaaSiAuthApiImpl(Session session, boolean isSukebei) { 33 | super(isSukebei); 34 | this.session = session; 35 | } 36 | 37 | public Session getSession() { 38 | return session; 39 | } 40 | 41 | private HttpResponse fetchAccountInfoPage() { 42 | return fetchAccountInfoPage(new Cookie[] { session.toCookie()}); 43 | } 44 | 45 | private HttpResponse fetchAccountInfoPage(Cookie[] cookies) { 46 | HttpGet get = new HttpGet("https://" + domain + "/profile"); 47 | get.setConfig(HttpUtil.WITH_TIMEOUT); 48 | 49 | return HttpUtil.executeRequest(get, cookies); 50 | } 51 | 52 | @Override 53 | public AccountInfo getAccountInfo() { 54 | HttpResponse response = fetchAccountInfoPage(); 55 | return parsePage(response, new AccountInfoParser()); 56 | } 57 | 58 | @Override 59 | public void changeEmail(String currentPassword, String newEmail) { 60 | String csrfToken = parsePage(fetchAccountInfoPage(), new AccountInfoCsrfTokenParser()).getEmailToken(); 61 | 62 | HttpPost post = new HttpPost("https://" + domain + "/profile"); 63 | post.setConfig(HttpUtil.WITH_TIMEOUT); 64 | 65 | List form = new ArrayList<>(); 66 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 67 | form.add(new BasicNameValuePair("email", newEmail)); 68 | form.add(new BasicNameValuePair("current_password", currentPassword)); 69 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 70 | 71 | CookieStore cookieStore = new BasicCookieStore(); 72 | cookieStore.addCookie(session.toCookie()); 73 | HttpUtil.executeRequest(post, cookieStore); 74 | 75 | HttpResponse response = fetchAccountInfoPage(cookieStore.getCookies().toArray(new Cookie[0])); 76 | parsePage(response, new ValidateEmailChange()); 77 | } 78 | 79 | @Override 80 | public void changePassword(String currentPassword, String newPassword) { 81 | if(currentPassword.isEmpty()) 82 | throw new LoginException(); 83 | 84 | String csrfToken = parsePage(fetchAccountInfoPage(), new AccountInfoCsrfTokenParser()).getPasswordToken(); 85 | HttpPost post = new HttpPost("https://" + domain + "/profile"); 86 | post.setConfig(HttpUtil.WITH_TIMEOUT); 87 | 88 | List form = new ArrayList<>(); 89 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 90 | form.add(new BasicNameValuePair("current_password", currentPassword)); 91 | form.add(new BasicNameValuePair("new_password", newPassword)); 92 | form.add(new BasicNameValuePair("password_confirm", newPassword)); 93 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 94 | 95 | CookieStore cookieStore = new BasicCookieStore(); 96 | cookieStore.addCookie(session.toCookie()); 97 | HttpResponse response = HttpUtil.executeRequest(post, cookieStore); 98 | 99 | int statusCode = response.getStatusLine().getStatusCode(); 100 | if(statusCode == 302) { 101 | response = fetchAccountInfoPage(cookieStore.getCookies().toArray(new Cookie[0])); 102 | parsePage(response, new ValidatePasswordChange()); 103 | } else if(statusCode != 200) { 104 | throw new HttpErrorCodeException(statusCode); 105 | } 106 | 107 | parsePage(response, new ValidatePasswordChange()); 108 | } 109 | 110 | private String newUploadCsrfToken() { 111 | HttpGet get = new HttpGet("https://" + domain + "/upload"); 112 | get.setConfig(HttpUtil.WITH_TIMEOUT); 113 | 114 | HttpResponse response = HttpUtil.executeRequest(get, new Cookie[] { session.toCookie() }); 115 | return parsePage(response, new UploadCsrfTokenParser()); 116 | } 117 | 118 | @Override 119 | public int uploadTorrent(UploadTorrentRequest request) { 120 | HttpPost post = new HttpPost("https://" + domain + "/upload"); 121 | post.setConfig(HttpUtil.WITH_TIMEOUT); 122 | 123 | MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 124 | 125 | builder.addTextBody("csrf_token", newUploadCsrfToken()); 126 | 127 | ContentType torrentMime = ContentType.create("application/x-bittorrent"); 128 | builder.addBinaryBody("torrent_file", request.getSeedfile(), torrentMime, request.getSeedfile().getName()); 129 | 130 | builder.addTextBody("display_name", request.getName()); 131 | 132 | SubCategory c = request.getCategory(); 133 | if(c.isSukebei() != isSukebei) 134 | throw new IllegalCategoryException(); 135 | builder.addTextBody("category", c.getMainCategoryId() + "_" + c.getSubCategoryId()); 136 | 137 | builder.addTextBody("information", request.getInformation().orElse("")); 138 | 139 | if(request.isAnonymous()) 140 | builder.addTextBody("is_anonymous", "y"); 141 | 142 | if(request.isHidden()) 143 | builder.addTextBody("is_hidden", "y"); 144 | 145 | if(request.isRemake()) 146 | builder.addTextBody("is_remake", "y"); 147 | 148 | if(request.isCompleted()) 149 | builder.addTextBody("is_complete", "y"); 150 | 151 | builder.addTextBody("description", request.getDescription().orElse("")); 152 | 153 | post.setEntity(builder.build()); 154 | 155 | HttpResponse response = HttpUtil.executeRequest(post, new Cookie[] { session.toCookie() }); 156 | 157 | parsePage(response, new ValidateUploadResponse()); 158 | 159 | return parseViewUrl(response.getFirstHeader("Location").getValue()); 160 | } 161 | 162 | private int parseViewUrl(String viewUrl) { 163 | Matcher matcher = VIEW_URL_PATTERN.matcher(viewUrl); 164 | if(matcher.matches()) { 165 | return Integer.parseInt(matcher.group(1)); 166 | } else { 167 | throw new WebScrapeException("Cannot parse view url"); 168 | } 169 | } 170 | 171 | private String newDeleteCsrfToken(int torrentId) { 172 | HttpGet get = new HttpGet("https://" + domain + "/view/" + torrentId + "/edit"); 173 | get.setConfig(HttpUtil.WITH_TIMEOUT); 174 | 175 | HttpResponse response = HttpUtil.executeRequest(get, new Cookie[] { session.toCookie() }); 176 | int statusCode = response.getStatusLine().getStatusCode(); 177 | switch (statusCode) { 178 | case 200: return parsePage(response, new DeleteCsrfTokenParser()); 179 | case 403: throw new PermissionException(); 180 | case 404: throw new NoSuchTorrentException(torrentId); 181 | default: throw new HttpErrorCodeException(statusCode); 182 | } 183 | } 184 | 185 | @Override 186 | public void deleteTorrent(int torrentId) { 187 | String csrfToken = newDeleteCsrfToken(torrentId); 188 | 189 | HttpPost post = new HttpPost("https://" + domain + "/view/" + torrentId + "/edit"); 190 | post.setConfig(HttpUtil.WITH_TIMEOUT); 191 | 192 | List form = new ArrayList<>(); 193 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 194 | form.add(new BasicNameValuePair("delete", "Delete")); 195 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 196 | 197 | HttpUtil.executeRequest(post, new Cookie[] { session.toCookie() }); 198 | } 199 | 200 | @Override 201 | public void editTorrent(int torrentId, Consumer f) { 202 | EditTorrentRequest request = newEditRequest(torrentId); 203 | f.accept(request); 204 | if(request.getCategory().isSukebei() != isSukebei) 205 | throw new IllegalCategoryException(); 206 | 207 | HttpPost post = new HttpPost("https://" + domain + "/view/" + torrentId + "/edit"); 208 | post.setConfig(HttpUtil.WITH_TIMEOUT); 209 | 210 | SubCategory c = request.getCategory(); 211 | MultipartEntityBuilder builder = MultipartEntityBuilder.create(); 212 | builder.addTextBody("csrf_token", request.getCsrfToken()); 213 | builder.addTextBody("display_name", request.getName()); 214 | builder.addTextBody("category", c.getMainCategoryId() + "_" + c.getSubCategoryId()); 215 | builder.addTextBody("information", request.getInformation()); 216 | if(request.isAnonymous())builder.addTextBody("is_anonymous", "y"); 217 | if(request.isHidden())builder.addTextBody("is_hidden", "y"); 218 | if(request.isRemake())builder.addTextBody("is_remake", "y"); 219 | if(request.isCompleted())builder.addTextBody("is_complete", "y"); 220 | builder.addTextBody("description", request.getDescription()); 221 | builder.addTextBody("submit", "Save Changes"); 222 | post.setEntity(builder.build()); 223 | 224 | HttpUtil.executeRequest(post, new Cookie[] { session.toCookie()} ); 225 | } 226 | 227 | private EditTorrentRequest newEditRequest(int torrentId) { 228 | HttpGet get = new HttpGet("https://" + domain + "/view/" + torrentId + "/edit"); 229 | get.setConfig(HttpUtil.WITH_TIMEOUT); 230 | 231 | HttpResponse response = HttpUtil.executeRequest(get, new Cookie[] { session.toCookie() }); 232 | 233 | int statusCode = response.getStatusLine().getStatusCode(); 234 | switch (statusCode) { 235 | case 200: return parsePage(response, new EditTorrentParser()); 236 | case 403: throw new PermissionException(); 237 | case 404: throw new NoSuchTorrentException(torrentId); 238 | default: throw new HttpErrorCodeException(statusCode); 239 | } 240 | } 241 | 242 | private HttpResponse fetchViewTorrentPage(int torrentId) { 243 | CookieStore cookieStore = new BasicCookieStore(); 244 | cookieStore.addCookie(session.toCookie()); 245 | return fetchViewTorrentPage(torrentId, cookieStore); 246 | } 247 | 248 | private HttpResponse fetchViewTorrentPage(int torrentId, CookieStore store) { 249 | HttpGet get = new HttpGet("https://" + domain + "/view/" + torrentId); 250 | get.setConfig(HttpUtil.WITH_TIMEOUT); 251 | 252 | HttpResponse response = HttpUtil.executeRequest(get, store); 253 | 254 | int statusCode = response.getStatusLine().getStatusCode(); 255 | switch (statusCode) { 256 | case 200: return response; 257 | case 404: throw new NoSuchTorrentException(torrentId); 258 | default: throw new HttpErrorCodeException(statusCode); 259 | } 260 | } 261 | 262 | private String newWriteCommentCsrfToken(int torrentId) { 263 | HttpResponse response = fetchViewTorrentPage(torrentId); 264 | return parsePage(response, new WriteCommentCsrfTokenParser()); 265 | } 266 | 267 | @Override 268 | public int writeComment(int torrentId, String message) { 269 | String csrfToken = newWriteCommentCsrfToken(torrentId); 270 | 271 | HttpPost post = new HttpPost("https://" + domain + "/view/" + torrentId); 272 | post.setConfig(HttpUtil.WITH_TIMEOUT); 273 | 274 | List form = new ArrayList<>(); 275 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 276 | form.add(new BasicNameValuePair("comment", message)); 277 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 278 | 279 | CookieStore cookieStore = new BasicCookieStore(); 280 | cookieStore.addCookie(session.toCookie()); 281 | HttpResponse response = HttpUtil.executeRequest(post, cookieStore); 282 | 283 | int statusCode = response.getStatusLine().getStatusCode(); 284 | if(statusCode == 302) { 285 | String redirectUrl = response.getFirstHeader("Location").getValue(); 286 | response = fetchViewTorrentPage(torrentId, cookieStore); 287 | return parsePage(response, new WriteCommentResponseParser(redirectUrl)); 288 | } else { 289 | throw new HttpErrorCodeException(statusCode); 290 | } 291 | } 292 | 293 | private String newEditCommentCsrfToken(int torrentId, int commentId) { 294 | HttpResponse response = fetchViewTorrentPage(torrentId); 295 | return parsePage(response, new EditCommentCsrfTokenParser(commentId)); 296 | } 297 | 298 | @Override 299 | public void editComment(int torrentId, int commentId, String newMessage) { 300 | String csrfToken = newEditCommentCsrfToken(torrentId, commentId); 301 | 302 | HttpPost post = new HttpPost("https://" + domain + "/view/" + torrentId + "/comment/" + commentId + "/edit"); 303 | 304 | List form = new ArrayList<>(); 305 | form.add(new BasicNameValuePair("csrf_token", csrfToken)); 306 | form.add(new BasicNameValuePair("comment", newMessage)); 307 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 308 | 309 | HttpResponse response = HttpUtil.executeRequest(post, new Cookie[] { session.toCookie() }); 310 | 311 | int statusCode = response.getStatusLine().getStatusCode(); 312 | switch (statusCode) { 313 | case 200: return; 314 | case 400: throw new CannotEditException(); 315 | default: throw new HttpErrorCodeException(statusCode); 316 | } 317 | } 318 | 319 | @Override 320 | public void deleteComment(int torrentId, int commentId) { 321 | HttpPost post = new HttpPost("https://" + domain + "/view/" + torrentId + "/comment/" + commentId + "/delete"); 322 | post.setConfig(HttpUtil.WITH_TIMEOUT); 323 | 324 | List form = new ArrayList<>(); 325 | form.add(new BasicNameValuePair("submit", "")); 326 | post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); 327 | 328 | CookieStore cookieStore = new BasicCookieStore(); 329 | cookieStore.addCookie(session.toCookie()); 330 | HttpResponse response = HttpUtil.executeRequest(post, cookieStore); 331 | 332 | int statusCode = response.getStatusLine().getStatusCode(); 333 | switch (statusCode) { 334 | case 302: 335 | response = fetchViewTorrentPage(torrentId, cookieStore); 336 | parsePage(response, new ValidateDeleteComment()); 337 | return; 338 | case 403: throw new PermissionException(); 339 | case 404: throw new NoSuchCommentException(); 340 | default: throw new HttpErrorCodeException(statusCode); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/AccountInfoExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 4 | import de.kaysubs.tracker.nyaasi.model.AccountInfo; 5 | 6 | public class AccountInfoExample { 7 | 8 | public static void main(String[] args) { 9 | NyaaSiAuthApi api = LoginExample.login(); 10 | 11 | AccountInfo info = api.getAccountInfo(); 12 | 13 | System.out.println("name: " + info.getName()); 14 | System.out.println("email: " + info.getEmail()); 15 | System.out.println("avatar: " + info.getAvatarUrl()); 16 | System.out.println("user id: " + info.getUserId()); 17 | System.out.println("user class: " + info.getUserClass()); 18 | System.out.println("registration date: " + info.getRegistrationDate()); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/ChangeEmailExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 5 | 6 | import java.util.Scanner; 7 | 8 | public class ChangeEmailExample { 9 | 10 | public static void main(String[] args) { 11 | Scanner sc = new Scanner(System.in); 12 | System.out.print("login at sukebei [true/false]: "); 13 | boolean isSukebei = sc.nextBoolean(); 14 | System.out.print("username: "); 15 | String username = sc.next(); 16 | System.out.print("password: "); 17 | String password = sc.next(); 18 | System.out.print("new email: "); 19 | String newEmail = sc.next(); 20 | 21 | NyaaSiApi api = isSukebei ? NyaaSiApi.getSukebei() : NyaaSiApi.getNyaa(); 22 | NyaaSiAuthApi authApi = api.login(username, password); 23 | 24 | authApi.changeEmail(password, newEmail); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/ChangePasswordExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 5 | 6 | import java.util.Scanner; 7 | 8 | public class ChangePasswordExample { 9 | 10 | public static void main(String[] args) { 11 | Scanner sc = new Scanner(System.in); 12 | System.out.print("login at sukebei [true/false]: "); 13 | boolean isSukebei = sc.nextBoolean(); 14 | System.out.print("username: "); 15 | String username = sc.next(); 16 | System.out.print("password: "); 17 | String password = sc.next(); 18 | System.out.print("new password: "); 19 | String newPassword = sc.next(); 20 | 21 | NyaaSiApi api = isSukebei ? NyaaSiApi.getSukebei() : NyaaSiApi.getNyaa(); 22 | NyaaSiAuthApi authApi = api.login(username, password); 23 | 24 | authApi.changePassword(password, newPassword); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/CommentExamples.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 4 | 5 | import java.util.Scanner; 6 | 7 | public class CommentExamples { 8 | 9 | public static void main(String[] args) { 10 | NyaaSiAuthApi api = LoginExample.login(); 11 | Scanner sc = new Scanner(System.in); 12 | System.out.print("torrent id: "); 13 | int torrentId = sc.nextInt(); 14 | 15 | int commentId = api.writeComment(torrentId, "I'm testing a nyaa.si api"); 16 | System.out.println("Example comment has id " + commentId); 17 | 18 | api.editComment(torrentId, commentId, "I'm an edited comment."); 19 | System.out.println("The comment has been edited"); 20 | 21 | api.deleteComment(torrentId, commentId); 22 | System.out.println("Deleted example comment again"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/LoginExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 5 | 6 | import java.util.Scanner; 7 | 8 | public class LoginExample { 9 | public static void main(String[] args) { 10 | login(); 11 | } 12 | 13 | public static NyaaSiAuthApi login() { 14 | Scanner sc = new Scanner(System.in); 15 | System.out.print("login at sukebei [true/false]: "); 16 | boolean isSukebei = sc.nextBoolean(); 17 | System.out.print("username: "); 18 | String username = sc.next(); 19 | System.out.print("password: "); 20 | String password = sc.next(); 21 | 22 | NyaaSiApi api = isSukebei ? NyaaSiApi.getSukebei() : NyaaSiApi.getNyaa(); 23 | return api.login(username, password); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/SearchExamples.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.model.*; 5 | import de.kaysubs.tracker.nyaasi.util.SearchIterator; 6 | 7 | import java.util.Iterator; 8 | 9 | public class SearchExamples { 10 | 11 | public static void printLatestTorrents() { 12 | TorrentPreview[] nyaaTorrents = NyaaSiApi.getNyaa().search(new SearchRequest()); 13 | TorrentPreview[] sukebeiTorrents = NyaaSiApi.getSukebei().search(new SearchRequest()); 14 | 15 | System.out.println("Here are the latest uploads on nyaa.si:"); 16 | for(TorrentPreview torrent : nyaaTorrents) 17 | System.out.print(torrent.getTitle()); 18 | 19 | System.out.println("Here are the latest uploads on sukebei.nyaa.si:"); 20 | for(TorrentPreview torrent : sukebeiTorrents) 21 | System.out.print(torrent.getTitle()); 22 | } 23 | 24 | public static void getDetailsOfAResult() { 25 | TorrentPreview latestUpload = NyaaSiApi.getNyaa().search(new SearchRequest())[0]; 26 | 27 | TorrentInfo info = NyaaSiApi.getNyaa().getTorrentInfo(latestUpload.getId()); 28 | 29 | System.out.println("The latest torrent was uploaded by the user " + info.getUploader()); 30 | } 31 | 32 | public static void searchForTorrent() { 33 | NyaaSiApi.getNyaa().search(new SearchRequest().setTerm("Overlord")); 34 | } 35 | 36 | public static void iterateOverSearchResult() { 37 | Iterator iter = new SearchIterator(NyaaSiApi.getNyaa(), 38 | new SearchRequest().setTerm("Overlord")); 39 | 40 | iter.forEachRemaining(torrent -> 41 | System.out.println(torrent.getTitle())); 42 | } 43 | 44 | public static void filterByCategory() { 45 | NyaaSiApi.getNyaa().search(new SearchRequest() 46 | .setCategory(MainCategory.Nyaa.anime)); 47 | 48 | NyaaSiApi.getNyaa().search(new SearchRequest() 49 | .setCategory(Category.Nyaa.anime.english)); 50 | 51 | // Use Category.Sukebei when searching on sukebei.nyaa.si 52 | NyaaSiApi.getSukebei().search(new SearchRequest() 53 | .setCategory(Category.Sukebei.art.anime)); 54 | } 55 | 56 | public static void usingFilters() { 57 | // Do not shows torrents that are marked as remakes 58 | NyaaSiApi.getNyaa().search(new SearchRequest() 59 | .setFilter(SearchRequest.Filter.NO_REMAKES)); 60 | 61 | // Show only torrents uploaded by trusted users 62 | NyaaSiApi.getNyaa().search(new SearchRequest() 63 | .setFilter(SearchRequest.Filter.TRUSTED_ONLY)); 64 | } 65 | 66 | public static void listTorrentsOfUser(String username) { 67 | NyaaSiApi.getNyaa().search(new SearchRequest().setUser(username)); 68 | NyaaSiApi.getSukebei().search(new SearchRequest().setUser(username)); 69 | } 70 | 71 | public static void orderingAndSorting() { 72 | TorrentPreview oldestTorrent = NyaaSiApi.getNyaa().search(new SearchRequest() 73 | // Sort by age 74 | .setSortedBy(SearchRequest.Sort.DATE) 75 | // Show in ascending order (oldest first, latest last) 76 | .setOrdering(SearchRequest.Ordering.ASCENDING))[0]; 77 | 78 | TorrentPreview largestTorrent = NyaaSiApi.getNyaa().search(new SearchRequest() 79 | .setSortedBy(SearchRequest.Sort.SIZE) 80 | .setOrdering(SearchRequest.Ordering.DESCENDING))[0]; 81 | 82 | TorrentPreview smallestTorrent = NyaaSiApi.getNyaa().search(new SearchRequest() 83 | .setSortedBy(SearchRequest.Sort.SIZE) 84 | .setOrdering(SearchRequest.Ordering.ASCENDING))[0]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/TorrentInfoExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.model.TorrentInfo; 5 | 6 | import java.util.Scanner; 7 | 8 | public class TorrentInfoExample { 9 | 10 | public static void main(String[] args) { 11 | Scanner sc = new Scanner(System.in); 12 | System.out.print("is the torrent on sukebei [true/false]: "); 13 | boolean isSubekei = sc.nextBoolean(); 14 | System.out.print("torrent id: "); 15 | int torrentId = sc.nextInt(); 16 | 17 | NyaaSiApi api = isSubekei ? NyaaSiApi.getSukebei() : NyaaSiApi.getNyaa(); 18 | TorrentInfo info = api.getTorrentInfo(torrentId); 19 | 20 | System.out.println("title: " + info.getTitle()); 21 | System.out.println("category: " + info.getCategory().getMainCategory() + "." + info.getCategory()); 22 | System.out.println("size: " + info.getSize()); 23 | System.out.println("upload date: " + info.getDate()); 24 | System.out.println("uploader: " + info.getUploader().orElse("Anonymous")); 25 | System.out.println("state: " + info.getTorrentState()); 26 | System.out.println("seeders: " + info.getSeeders()); 27 | System.out.println("leechers: " + info.getLeechers()); 28 | System.out.println("completed: " + info.getCompleted()); 29 | System.out.println("homepage/irc/etc.: " + info.getInformation()); 30 | System.out.println("hash: " + info.getHash()); 31 | System.out.println("download link: " + info.getDownloadLink()); 32 | System.out.println("magnet link: " + info.getMagnetLink()); 33 | System.out.println("description: " + info.getDescription()); 34 | System.out.println("files:"); 35 | printFile(info.getFile(), 0); 36 | 37 | for(TorrentInfo.Comment comment : info.getComments()) { 38 | System.out.println("[Comment]"); 39 | System.out.println("user: " + comment.getUsername()); 40 | System.out.println("trusted: " + comment.isTrusted()); 41 | System.out.println("avatar: " + comment.getAvatar()); 42 | System.out.println("date: " + comment.getDate()); 43 | System.out.println(comment.getComment()); 44 | System.out.println(); 45 | } 46 | } 47 | 48 | private static void printFile(TorrentInfo.FileNode file, int offset) { 49 | for (int i = 0; i < offset; i++) 50 | System.out.print(" "); 51 | 52 | if(file instanceof TorrentInfo.File) { 53 | System.out.println(file.getName() + " (" + ((TorrentInfo.File) file).getSize() + ")"); 54 | } else { 55 | System.out.println(file.getName()); 56 | 57 | for(TorrentInfo.FileNode child : ((TorrentInfo.Folder) file).getChildren()) { 58 | printFile(child, offset + 1); 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/examples/UploadExample.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.examples; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.NyaaSiAuthApi; 5 | import de.kaysubs.tracker.nyaasi.model.Category; 6 | import de.kaysubs.tracker.nyaasi.model.SubCategory; 7 | import de.kaysubs.tracker.nyaasi.model.UploadTorrentRequest; 8 | 9 | import java.io.File; 10 | import java.util.Scanner; 11 | 12 | public class UploadExample { 13 | 14 | public static void main(String[] args) { 15 | NyaaSiAuthApi api = LoginExample.login(); 16 | Scanner sc = new Scanner(System.in); 17 | System.out.print("path to .torrent file: "); 18 | File seedFile = new File(sc.next()); 19 | 20 | int torrentId = uploadTorrent(api, seedFile); 21 | editTorrent(api, torrentId); 22 | deleteTorrent(api, torrentId); 23 | } 24 | 25 | private static int uploadTorrent(NyaaSiAuthApi api, File seedfile) { 26 | SubCategory category = api.isSukebei() ? 27 | Category.Sukebei.art.games : 28 | Category.Nyaa.software.games; 29 | 30 | int torrentId = api.uploadTorrent(new UploadTorrentRequest(seedfile, "API test torrent", category) 31 | .setDescription("This is an example torrent uploaded through 'k subs nyaa.si API.") 32 | .setInformation("http://example.com") // Link to your Homepage 33 | .setHidden(true) // this torrent should not be seen on the startpage 34 | .setAnonymous(true) // other users cannot see who uploaded this torrent 35 | ); 36 | 37 | System.out.println("The example torrent was uploaded under the id " + torrentId); 38 | return torrentId; 39 | } 40 | 41 | private static void editTorrent(NyaaSiAuthApi api, int torrentId) { 42 | api.editTorrent(torrentId, torrent -> torrent 43 | .setName("EditedAPI test torrent") 44 | .setDescription("This torrent has been edited")); 45 | 46 | System.out.print("The example torrent was edited"); 47 | } 48 | 49 | private static void deleteTorrent(NyaaSiAuthApi api, int torrentId) { 50 | api.deleteTorrent(torrentId); 51 | System.out.print("The example torrent was deleted again."); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/CannotEditException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class CannotEditException extends NyaaSiException {} 4 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/CaptchaException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class CaptchaException extends NyaaSiException {} 4 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/IllegalCategoryException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class IllegalCategoryException extends NyaaSiException { 4 | 5 | public IllegalCategoryException() {} 6 | 7 | public IllegalCategoryException(String message) { 8 | super(message); 9 | } 10 | 11 | public IllegalCategoryException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public IllegalCategoryException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public IllegalCategoryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/LoginException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class LoginException extends NyaaSiException {} 4 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/MissingTrackerException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class MissingTrackerException extends NyaaSiException { 4 | private final String tracker; 5 | 6 | public MissingTrackerException(String tracker) { 7 | super(tracker); 8 | this.tracker = tracker; 9 | } 10 | 11 | public String getTracker() { 12 | return tracker; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/NoSuchCommentException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class NoSuchCommentException extends NyaaSiException {} 4 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/NoSuchTorrentException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class NoSuchTorrentException extends NyaaSiException { 4 | private final int torrentId; 5 | 6 | public NoSuchTorrentException(int torrentId) { 7 | super(Integer.toString(torrentId)); 8 | this.torrentId = torrentId; 9 | } 10 | 11 | public int getTorrentId() { 12 | return torrentId; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/NyaaSiException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class NyaaSiException extends RuntimeException { 4 | 5 | public NyaaSiException() {} 6 | 7 | public NyaaSiException(String message) { 8 | super(message); 9 | } 10 | 11 | public NyaaSiException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public NyaaSiException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public NyaaSiException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/PasswordLengthException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class PasswordLengthException extends NyaaSiException {} 4 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/PermissionException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class PermissionException extends NyaaSiException { 4 | 5 | public PermissionException() {} 6 | 7 | public PermissionException(String message) { 8 | super(message); 9 | } 10 | 11 | public PermissionException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public PermissionException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public PermissionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/exception/WebScrapeException.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.exception; 2 | 3 | public class WebScrapeException extends NyaaSiException { 4 | 5 | public WebScrapeException() {} 6 | 7 | public WebScrapeException(String message) { 8 | super(message); 9 | } 10 | 11 | public WebScrapeException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public WebScrapeException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public WebScrapeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/AccountInfo.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.util.Date; 4 | 5 | public class AccountInfo { 6 | 7 | private final String name; 8 | private final int userId; 9 | private final Date registrationDate; 10 | private final String avatarUrl; 11 | private final String userClass; 12 | private final String email; 13 | 14 | public AccountInfo(String name, int userId, Date registrationDate, String avatarUrl, String userClass, String email) { 15 | this.name = name; 16 | this.userId = userId; 17 | this.registrationDate = registrationDate; 18 | this.avatarUrl = avatarUrl; 19 | this.userClass = userClass; 20 | this.email = email; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public int getUserId() { 28 | return userId; 29 | } 30 | 31 | public Date getRegistrationDate() { 32 | return registrationDate; 33 | } 34 | 35 | public String getAvatarUrl() { 36 | return avatarUrl; 37 | } 38 | 39 | public String getUserClass() { 40 | return userClass; 41 | } 42 | 43 | public String getEmail() { 44 | return email; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/Category.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.util.Arrays; 4 | 5 | public interface Category { 6 | int getMainCategoryId(); 7 | int getSubCategoryId(); 8 | boolean isSukebei(); 9 | 10 | interface Nyaa extends Category { 11 | default boolean isSukebei() { 12 | return false; 13 | } 14 | 15 | AnimeMainCategory anime = new AnimeMainCategory(); 16 | AudioMainCategory audio = new AudioMainCategory(); 17 | LiteratureMainCategory literature = new LiteratureMainCategory(); 18 | LiveActionMainCategory liveAction = new LiveActionMainCategory(); 19 | PicturesMainCategory pictures = new PicturesMainCategory(); 20 | SoftwareMainCategory software = new SoftwareMainCategory(); 21 | 22 | MainCategory[] mainCategories = new MainCategory[] { 23 | anime, audio, literature, liveAction, pictures, software 24 | }; 25 | 26 | static MainCategory fromId(int mainCategoryId) { 27 | return Arrays.stream(Nyaa.mainCategories) 28 | .filter(c -> c.getMainCategoryId() == mainCategoryId).findFirst() 29 | .orElseThrow(() -> new IllegalArgumentException("No nyaa MainCategory with id " + mainCategoryId)); 30 | } 31 | 32 | class AnimeMainCategory extends MainCategory implements Category.Nyaa { 33 | public final SubCategory amv = new SubCategory(this, "AMV", 1); 34 | public final SubCategory english = new SubCategory(this, "English", 2); 35 | public final SubCategory nonEnglish = new SubCategory(this, "NonEnglish", 3); 36 | public final SubCategory raw = new SubCategory(this, "Raw", 4); 37 | 38 | private AnimeMainCategory() { 39 | super(1); 40 | } 41 | } 42 | 43 | class AudioMainCategory extends MainCategory implements Category.Nyaa { 44 | public final SubCategory lossless = new SubCategory(this, "AMV", 1); 45 | public final SubCategory lossy = new SubCategory(this, "English", 2); 46 | 47 | private AudioMainCategory() { 48 | super(2); 49 | } 50 | } 51 | 52 | class LiteratureMainCategory extends MainCategory implements Nyaa { 53 | public final SubCategory english = new SubCategory(this, "English", 1); 54 | public final SubCategory nonEnglish = new SubCategory(this, "NonEnglish", 2); 55 | public final SubCategory raw = new SubCategory(this, "Raw", 3); 56 | 57 | private LiteratureMainCategory() { 58 | super(3); 59 | } 60 | } 61 | 62 | class LiveActionMainCategory extends MainCategory implements Nyaa { 63 | public final SubCategory english = new SubCategory(this, "English", 1); 64 | public final SubCategory idol = new SubCategory(this, "Idol", 2); 65 | public final SubCategory nonEnglish = new SubCategory(this, "NonEnglish", 3); 66 | public final SubCategory raw = new SubCategory(this, "Raw", 4); 67 | 68 | private LiveActionMainCategory() { 69 | super(4); 70 | } 71 | } 72 | 73 | class PicturesMainCategory extends MainCategory implements Nyaa { 74 | public final SubCategory graphics = new SubCategory(this, "Graphics", 1); 75 | public final SubCategory photos = new SubCategory(this, "Photos", 2); 76 | 77 | private PicturesMainCategory() { 78 | super(5); 79 | } 80 | } 81 | 82 | class SoftwareMainCategory extends MainCategory implements Nyaa { 83 | public final SubCategory applications = new SubCategory(this, "Applications", 1); 84 | public final SubCategory games = new SubCategory(this, "Games", 2); 85 | 86 | private SoftwareMainCategory() { 87 | super(6); 88 | } 89 | } 90 | } 91 | 92 | interface Sukebei extends Category { 93 | default boolean isSukebei() { 94 | return true; 95 | } 96 | 97 | ArtMainCategory art = new ArtMainCategory(); 98 | RealLifeMainCategory realLife = new RealLifeMainCategory(); 99 | 100 | MainCategory[] mainCategories = new MainCategory[] { 101 | art, realLife 102 | }; 103 | 104 | static MainCategory fromId(int mainCategoryId) { 105 | return Arrays.stream(Sukebei.mainCategories) 106 | .filter(c -> c.getMainCategoryId() == mainCategoryId).findFirst() 107 | .orElseThrow(() -> new IllegalArgumentException("No sukebei MainCategory with id " + mainCategoryId)); 108 | } 109 | 110 | class ArtMainCategory extends MainCategory implements Sukebei { 111 | public final SubCategory anime = new SubCategory(this, "Anime", 1); 112 | public final SubCategory doujinshi = new SubCategory(this, "Doujinshi", 2); 113 | public final SubCategory games = new SubCategory(this, "Games", 3); 114 | public final SubCategory manga = new SubCategory(this, "Manga", 4); 115 | public final SubCategory pictures = new SubCategory(this, "Pictures", 5); 116 | 117 | private ArtMainCategory() { 118 | super(1); 119 | } 120 | } 121 | 122 | class RealLifeMainCategory extends MainCategory implements Sukebei { 123 | public final SubCategory pictures = new SubCategory(this, "Pictures", 1); 124 | public final SubCategory videos = new SubCategory(this, "Videos", 2); 125 | 126 | private RealLifeMainCategory() { 127 | super(2); 128 | } 129 | } 130 | 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/DataSize.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | public class DataSize { 4 | private final int value; 5 | private final DataUnit unit; 6 | 7 | public enum DataUnit { 8 | BYTE("Bytes"), 9 | KILOBYTE("KiB"), 10 | MEGABYTE("MiB"), 11 | GIGABYTE("GiB"), 12 | TERABYTE("TiB"); 13 | 14 | private final String unitName; 15 | 16 | DataUnit(String unitName) { 17 | this.unitName = unitName; 18 | } 19 | public String getUnitName() { 20 | return unitName; 21 | } 22 | } 23 | 24 | public DataSize(int value, DataUnit unit) { 25 | this.value = value; 26 | this.unit = unit; 27 | } 28 | 29 | public int getValue() { 30 | return value; 31 | } 32 | 33 | public DataUnit getUnit() { 34 | return unit; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return value + " " + unit.unitName; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/EditTorrentRequest.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | public class EditTorrentRequest { 4 | private final String csrfToken; 5 | private String name; 6 | private SubCategory category; 7 | private String information; 8 | private String description; 9 | private boolean isAnonymous; 10 | private boolean isHidden; 11 | private boolean isRemake; 12 | private boolean isCompleted; 13 | 14 | public EditTorrentRequest(String csrfToken, String name, SubCategory category, String information, String description, 15 | boolean isAnonymous, boolean isHidden, boolean isRemake, boolean isCompleted) { 16 | this.csrfToken = csrfToken; 17 | this.name = name; 18 | this.category = category; 19 | this.information = information; 20 | this.description = description; 21 | this.isAnonymous = isAnonymous; 22 | this.isHidden = isHidden; 23 | this.isRemake = isRemake; 24 | this.isCompleted = isCompleted; 25 | } 26 | 27 | public String getCsrfToken() { 28 | return csrfToken; 29 | } 30 | 31 | public SubCategory getCategory() { 32 | return category; 33 | } 34 | 35 | public EditTorrentRequest setCategory(SubCategory category) { 36 | this.category = category; 37 | return this; 38 | } 39 | 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public EditTorrentRequest setName(String name) { 45 | this.name = name; 46 | return this; 47 | } 48 | 49 | public String getInformation() { 50 | return information; 51 | } 52 | 53 | /** 54 | * Website, irc or similar 55 | */ 56 | public EditTorrentRequest setInformation(String information) { 57 | this.information = information; 58 | return this; 59 | } 60 | 61 | public String getDescription() { 62 | return description; 63 | } 64 | 65 | public EditTorrentRequest setDescription(String description) { 66 | this.description = description; 67 | return this; 68 | } 69 | 70 | public boolean isAnonymous() { 71 | return isAnonymous; 72 | } 73 | 74 | public EditTorrentRequest setAnonymous(boolean anonymous) { 75 | isAnonymous = anonymous; 76 | return this; 77 | } 78 | 79 | public boolean isHidden() { 80 | return isHidden; 81 | } 82 | 83 | public EditTorrentRequest setHidden(boolean hidden) { 84 | isHidden = hidden; 85 | return this; 86 | } 87 | 88 | public boolean isRemake() { 89 | return isRemake; 90 | } 91 | 92 | public EditTorrentRequest setRemake(boolean remake) { 93 | isRemake = remake; 94 | return this; 95 | } 96 | 97 | public boolean isCompleted() { 98 | return isCompleted; 99 | } 100 | 101 | public EditTorrentRequest setCompleted(boolean completed) { 102 | isCompleted = completed; 103 | return this; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/MainCategory.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.util.Arrays; 4 | 5 | public abstract class MainCategory implements Category { 6 | 7 | private final int id; 8 | private SubCategory[] subCategories; 9 | 10 | public MainCategory(int id) { 11 | this.id = id; 12 | } 13 | 14 | public SubCategory[] getSubCategories() { 15 | if(subCategories == null) { 16 | subCategories = Arrays.stream(getClass().getDeclaredFields()) 17 | .filter(field -> SubCategory.class.isAssignableFrom(field.getType())) 18 | .map(field -> { 19 | try { 20 | return (SubCategory) field.get(this); 21 | } catch (IllegalAccessException e) { 22 | throw new IllegalStateException("This should never happen. All fields are public.", e); 23 | } 24 | }).toArray(SubCategory[]::new); 25 | } 26 | 27 | return subCategories; 28 | } 29 | 30 | public int getMainCategoryId() { 31 | return id; 32 | } 33 | 34 | public int getSubCategoryId() { 35 | return 0; 36 | } 37 | 38 | public SubCategory getSubcategoryFromId(int subId) { 39 | for (SubCategory subCategory : getSubCategories()) 40 | if (subCategory.getSubCategoryId() == subId) 41 | return subCategory; 42 | 43 | throw new IllegalArgumentException("Category \"" + this.toString() + "\" has no subcategory with id " + subId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/SearchRequest.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.util.Optional; 4 | import java.util.OptionalInt; 5 | 6 | public class SearchRequest { 7 | private Optional term = Optional.empty(); 8 | private Optional category = Optional.empty(); 9 | private Optional filter = Optional.empty(); 10 | private Optional user = Optional.empty(); 11 | private OptionalInt page = OptionalInt.empty(); 12 | private Optional ordering = Optional.empty(); 13 | private Optional sortedBy = Optional.empty(); 14 | 15 | public enum Filter { 16 | NONE(0), NO_REMAKES(1), TRUSTED_ONLY(2); 17 | private final int id; 18 | 19 | Filter(int id) { 20 | this.id = id; 21 | } 22 | 23 | public int getId() { 24 | return id; 25 | } 26 | } 27 | 28 | public enum Ordering { 29 | ASCENDING("asc"), DESCENDING("desc"); 30 | private final String id; 31 | 32 | Ordering(String id) { 33 | this.id = id; 34 | } 35 | 36 | public String getId() { 37 | return id; 38 | } 39 | } 40 | 41 | public enum Sort { 42 | COMMENTS("comments"), 43 | SIZE("size"), 44 | DATE("id"), 45 | SEEDERS("seeders"), 46 | LEECHERS("seeders"), 47 | DOWNLOADS("downloads"); 48 | private final String id; 49 | 50 | Sort(String id) { 51 | this.id = id; 52 | } 53 | 54 | public String getId() { 55 | return id; 56 | } 57 | } 58 | 59 | public Optional getTerm() { 60 | return term; 61 | } 62 | 63 | public SearchRequest setTerm(String term) { 64 | this.term = Optional.ofNullable(term); 65 | return this; 66 | } 67 | 68 | public Optional getCategory() { 69 | return category; 70 | } 71 | 72 | public SearchRequest setCategory(Category category) { 73 | this.category = Optional.ofNullable(category); 74 | return this; 75 | } 76 | 77 | public Optional getFilter() { 78 | return filter; 79 | } 80 | 81 | public SearchRequest setFilter(Filter filter) { 82 | this.filter = Optional.ofNullable(filter); 83 | return this; 84 | } 85 | 86 | public Optional getUser() { 87 | return user; 88 | } 89 | 90 | public SearchRequest setUser(String user) { 91 | this.user = Optional.ofNullable(user); 92 | return this; 93 | } 94 | 95 | public OptionalInt getPage() { 96 | return page; 97 | } 98 | 99 | /** 100 | * Page of results to return, 101 | * where page 1 is the first one. 102 | */ 103 | public SearchRequest setPage(Integer page) { 104 | this.page = page == null ? OptionalInt.empty() : OptionalInt.of(page); 105 | return this; 106 | } 107 | 108 | public Optional getOrdering() { 109 | return ordering; 110 | } 111 | 112 | public SearchRequest setOrdering(Ordering ordering) { 113 | this.ordering = Optional.ofNullable(ordering); 114 | return this; 115 | } 116 | 117 | public Optional getSortedBy() { 118 | return sortedBy; 119 | } 120 | 121 | public SearchRequest setSortedBy(Sort sortBy) { 122 | this.sortedBy = Optional.ofNullable(sortBy); 123 | return this; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/Session.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import org.apache.http.cookie.Cookie; 4 | import org.apache.http.impl.cookie.BasicClientCookie; 5 | 6 | public class Session { 7 | 8 | private final String sessionId; 9 | private final boolean isSubekei; 10 | 11 | public Session(String sessionId, boolean isSubekei) { 12 | this.sessionId = sessionId; 13 | this.isSubekei = isSubekei; 14 | } 15 | 16 | public boolean isSubekei() { 17 | return isSubekei; 18 | } 19 | 20 | public String getSessionId() { 21 | return sessionId; 22 | } 23 | 24 | public Cookie toCookie() { 25 | BasicClientCookie cookie = new BasicClientCookie("session", sessionId); 26 | cookie.setPath("/"); 27 | cookie.setDomain(isSubekei ? "sukebei.nyaa.si" : "nyaa.si"); 28 | return cookie; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/SubCategory.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | public class SubCategory implements Category { 4 | private final MainCategory mainCategory; 5 | private final String name; 6 | private final int id; 7 | 8 | public SubCategory(MainCategory mainCategory, String name, int id) { 9 | this.mainCategory = mainCategory; 10 | this.name = name; 11 | this.id = id; 12 | } 13 | 14 | public String getName() { 15 | return name; 16 | } 17 | 18 | public MainCategory getMainCategory() { 19 | return mainCategory; 20 | } 21 | 22 | public int getSubCategoryId() { 23 | return id; 24 | } 25 | 26 | public int getMainCategoryId() { 27 | return mainCategory.getMainCategoryId(); 28 | } 29 | 30 | @Override 31 | public boolean isSukebei() { 32 | return mainCategory.isSukebei(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/TorrentInfo.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import org.jsoup.nodes.Element; 4 | 5 | import java.net.URI; 6 | import java.net.URL; 7 | import java.util.Date; 8 | import java.util.Optional; 9 | 10 | public class TorrentInfo { 11 | private String title; 12 | private Element descriptionDiv; 13 | private SubCategory category; 14 | private DataSize size; 15 | private Date date; 16 | private Optional uploader; 17 | private TorrentState torrentState; 18 | private int seeders; 19 | private int leechers; 20 | private int completed; 21 | private String information; 22 | private String hash; 23 | private URL downloadLink; 24 | private URI magnetLink; 25 | private FileNode file; 26 | private Comment[] comments; 27 | 28 | public static class Comment { 29 | private final int commentId; 30 | private final String username; 31 | private final boolean isTrusted; 32 | private final String avatar; 33 | private final Date date; 34 | private final Element commentDiv; 35 | 36 | public Comment(int commentId, String username, boolean isTrusted, String avatar, Date date, Element commentDiv) { 37 | this.commentId = commentId; 38 | this.username = username; 39 | this.isTrusted = isTrusted; 40 | this.avatar = avatar; 41 | this.date = date; 42 | this.commentDiv = commentDiv; 43 | } 44 | 45 | public int getCommentId() { 46 | return commentId; 47 | } 48 | 49 | public String getUsername() { 50 | return username; 51 | } 52 | 53 | public boolean isTrusted() { 54 | return isTrusted; 55 | } 56 | 57 | public String getAvatar() { 58 | return avatar; 59 | } 60 | 61 | public Date getDate() { 62 | return date; 63 | } 64 | 65 | public String getComment() { 66 | return getCommentDiv().text(); 67 | } 68 | 69 | public Element getCommentDiv() { 70 | return commentDiv; 71 | } 72 | } 73 | 74 | public interface FileNode { 75 | String getName(); 76 | } 77 | 78 | public static class File implements FileNode { 79 | private final String name; 80 | private final DataSize size; 81 | 82 | public File(String name, DataSize size) { 83 | this.name = name; 84 | this.size = size; 85 | } 86 | 87 | public String getName() { 88 | return name; 89 | } 90 | 91 | public DataSize getSize() { 92 | return size; 93 | } 94 | } 95 | 96 | public static class Folder implements FileNode { 97 | private final String name; 98 | private final FileNode[] children; 99 | 100 | public Folder(String name, FileNode[] children) { 101 | this.name = name; 102 | this.children = children; 103 | } 104 | 105 | public String getName() { 106 | return name; 107 | } 108 | 109 | public FileNode[] getChildren() { 110 | return children; 111 | } 112 | } 113 | 114 | public String getTitle() { 115 | return title; 116 | } 117 | 118 | public void setTitle(String title) { 119 | this.title = title; 120 | } 121 | 122 | public String getDescription() { 123 | return getDescriptionDiv().text(); 124 | } 125 | 126 | public Element getDescriptionDiv() { 127 | return descriptionDiv; 128 | } 129 | 130 | public void setDescriptionDiv(Element descriptionDiv) { 131 | this.descriptionDiv = descriptionDiv; 132 | } 133 | 134 | public SubCategory getCategory() { 135 | return category; 136 | } 137 | 138 | public void setCategory(SubCategory category) { 139 | this.category = category; 140 | } 141 | 142 | public DataSize getSize() { 143 | return size; 144 | } 145 | 146 | public void setSize(DataSize size) { 147 | this.size = size; 148 | } 149 | 150 | public Date getDate() { 151 | return date; 152 | } 153 | 154 | public void setDate(Date date) { 155 | this.date = date; 156 | } 157 | 158 | public Optional getUploader() { 159 | return uploader; 160 | } 161 | 162 | public void setUploader(Optional uploader) { 163 | this.uploader = uploader; 164 | } 165 | 166 | public TorrentState getTorrentState() { 167 | return torrentState; 168 | } 169 | 170 | public TorrentInfo setTorrentState(TorrentState torrentState) { 171 | this.torrentState = torrentState; 172 | return this; 173 | } 174 | 175 | public int getSeeders() { 176 | return seeders; 177 | } 178 | 179 | public void setSeeders(int seeders) { 180 | this.seeders = seeders; 181 | } 182 | 183 | public int getLeechers() { 184 | return leechers; 185 | } 186 | 187 | public void setLeechers(int leechers) { 188 | this.leechers = leechers; 189 | } 190 | 191 | public int getCompleted() { 192 | return completed; 193 | } 194 | 195 | public void setCompleted(int completed) { 196 | this.completed = completed; 197 | } 198 | 199 | public String getInformation() { 200 | return information; 201 | } 202 | 203 | public void setInformation(String information) { 204 | this.information = information; 205 | } 206 | 207 | public String getHash() { 208 | return hash; 209 | } 210 | 211 | public void setHash(String hash) { 212 | this.hash = hash; 213 | } 214 | 215 | public URL getDownloadLink() { 216 | return downloadLink; 217 | } 218 | 219 | public void setDownloadLink(URL downloadLink) { 220 | this.downloadLink = downloadLink; 221 | } 222 | 223 | public URI getMagnetLink() { 224 | return magnetLink; 225 | } 226 | 227 | public void setMagnetLink(URI magnetLink) { 228 | this.magnetLink = magnetLink; 229 | } 230 | 231 | public FileNode getFile() { 232 | return file; 233 | } 234 | 235 | public void setFile(FileNode file) { 236 | this.file = file; 237 | } 238 | 239 | public Comment[] getComments() { 240 | return comments; 241 | } 242 | 243 | public void setComments(Comment[] comments) { 244 | this.comments = comments; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/TorrentPreview.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.net.URI; 4 | import java.net.URL; 5 | import java.util.Date; 6 | 7 | public class TorrentPreview { 8 | private final int id; 9 | private final TorrentState torrentState; 10 | private final SubCategory category; 11 | private final String title; 12 | private final int commentCount; 13 | private final URL downloadLink; 14 | private final URI magnetLink; 15 | private final DataSize size; 16 | private final Date date; 17 | private final int seeders; 18 | private final int leechers; 19 | private final int completed; 20 | 21 | public TorrentPreview(int id, TorrentState torrentState, SubCategory category, String title, 22 | int commentCount, URL downloadLink, URI magnetLink, DataSize size, Date date, 23 | int seeders, int leechers, int completed) { 24 | this.id = id; 25 | this.torrentState = torrentState; 26 | this.category = category; 27 | this.title = title; 28 | this.commentCount = commentCount; 29 | this.downloadLink = downloadLink; 30 | this.magnetLink = magnetLink; 31 | this.size = size; 32 | this.date = date; 33 | this.seeders = seeders; 34 | this.leechers = leechers; 35 | this.completed = completed; 36 | } 37 | 38 | public int getId() { 39 | return id; 40 | } 41 | 42 | public TorrentState getTorrentState() { 43 | return torrentState; 44 | } 45 | 46 | public SubCategory getCategory() { 47 | return category; 48 | } 49 | 50 | public String getTitle() { 51 | return title; 52 | } 53 | 54 | public int getCommentCount() { 55 | return commentCount; 56 | } 57 | 58 | public URL getDownloadLink() { 59 | return downloadLink; 60 | } 61 | 62 | public URI getMagnetLink() { 63 | return magnetLink; 64 | } 65 | 66 | public DataSize getSize() { 67 | return size; 68 | } 69 | 70 | public Date getDate() { 71 | return date; 72 | } 73 | 74 | public int getSeeders() { 75 | return seeders; 76 | } 77 | 78 | public int getLeechers() { 79 | return leechers; 80 | } 81 | 82 | public int getCompleted() { 83 | return completed; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/TorrentState.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | public enum TorrentState { 4 | NORMAL, REMAKE, TRUSTED; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/model/UploadTorrentRequest.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.model; 2 | 3 | import java.io.File; 4 | import java.util.Optional; 5 | 6 | public class UploadTorrentRequest { 7 | private final File seedfile; 8 | private final String name; 9 | private final SubCategory category; 10 | private Optional information = Optional.empty(); 11 | private Optional description = Optional.empty(); 12 | private boolean isAnonymous = false; 13 | private boolean isHidden = false; 14 | private boolean isRemake = false; 15 | private boolean isCompleted = false; 16 | 17 | public UploadTorrentRequest(File seedfile, String name, SubCategory category) { 18 | this.seedfile = seedfile; 19 | this.name = name; 20 | this.category = category; 21 | } 22 | 23 | public File getSeedfile() { 24 | return seedfile; 25 | } 26 | 27 | public SubCategory getCategory() { 28 | return category; 29 | } 30 | 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | public Optional getInformation() { 36 | return information; 37 | } 38 | 39 | /** 40 | * Website, irc or similar 41 | */ 42 | public UploadTorrentRequest setInformation(String information) { 43 | this.information = Optional.ofNullable(information); 44 | return this; 45 | } 46 | 47 | public Optional getDescription() { 48 | return description; 49 | } 50 | 51 | public UploadTorrentRequest setDescription(String description) { 52 | this.description = Optional.ofNullable(description); 53 | return this; 54 | } 55 | 56 | public boolean isAnonymous() { 57 | return isAnonymous; 58 | } 59 | 60 | public UploadTorrentRequest setAnonymous(boolean anonymous) { 61 | isAnonymous = anonymous; 62 | return this; 63 | } 64 | 65 | public boolean isHidden() { 66 | return isHidden; 67 | } 68 | 69 | public UploadTorrentRequest setHidden(boolean hidden) { 70 | isHidden = hidden; 71 | return this; 72 | } 73 | 74 | public boolean isRemake() { 75 | return isRemake; 76 | } 77 | 78 | public UploadTorrentRequest setRemake(boolean remake) { 79 | isRemake = remake; 80 | return this; 81 | } 82 | 83 | public boolean isCompleted() { 84 | return isCompleted; 85 | } 86 | 87 | public UploadTorrentRequest setCompleted(boolean completed) { 88 | isCompleted = completed; 89 | return this; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/util/SearchIterator.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.util; 2 | 3 | import de.kaysubs.tracker.nyaasi.NyaaSiApi; 4 | import de.kaysubs.tracker.nyaasi.model.SearchRequest; 5 | import de.kaysubs.tracker.nyaasi.model.TorrentPreview; 6 | 7 | import java.util.Iterator; 8 | 9 | /** 10 | * Iterate over all search result. 11 | * This abstraction handles loading the next page for you. 12 | */ 13 | public class SearchIterator implements Iterator { 14 | private final SearchRequest request; 15 | private final NyaaSiApi api; 16 | 17 | private TorrentPreview[] cache = null; 18 | private int index = 0; 19 | 20 | public SearchIterator(NyaaSiApi api, SearchRequest request) { 21 | this.api = api; 22 | this.request = request; 23 | } 24 | 25 | private void validateCache() { 26 | if(cache == null || (cache.length > 0 && index >= cache.length)) { 27 | cache = api.search(request); 28 | index = 0; 29 | 30 | request.setPage(request.getPage().orElse(1) + 1); 31 | } 32 | } 33 | 34 | @Override 35 | public boolean hasNext() { 36 | validateCache(); 37 | return index < cache.length; 38 | } 39 | 40 | @Override 41 | public TorrentPreview next() { 42 | validateCache(); 43 | return cache[index++]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/AccountInfoCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import org.jsoup.nodes.Document; 4 | 5 | public class AccountInfoCsrfTokenParser implements Parser { 6 | @Override 7 | public Tokens parsePage(Document page, boolean isSukebei) { 8 | return new Tokens( 9 | ParseUtils.getCsrfToken(page.selectFirst("div#email-change")), 10 | ParseUtils.getCsrfToken(page.selectFirst("div#password-change"))); 11 | } 12 | 13 | public static class Tokens { 14 | private final String emailToken; 15 | private final String passwordToken; 16 | 17 | public Tokens(String emailToken, String passwordToken) { 18 | this.emailToken = emailToken; 19 | this.passwordToken = passwordToken; 20 | } 21 | 22 | public String getEmailToken() { 23 | return emailToken; 24 | } 25 | 26 | public String getPasswordToken() { 27 | return passwordToken; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/AccountInfoParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 4 | import de.kaysubs.tracker.nyaasi.model.AccountInfo; 5 | import org.jsoup.nodes.Document; 6 | import org.jsoup.nodes.Element; 7 | 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | public class AccountInfoParser implements Parser { 13 | private final SimpleDateFormat REGISTRATION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 14 | 15 | @Override 16 | public AccountInfo parsePage(Document page, boolean isSukebei) { 17 | String username = page.selectFirst("h2 > strong.text-default").text(); 18 | String avatarUrl = page.selectFirst("img.avatar").attr("src"); 19 | int userId = Integer.parseInt(getAfterDt(page, "User ID:").text()); 20 | String userClass = getAfterDt(page, "User Class:").text(); 21 | Date registrationDate = parseRegistrationDate(getAfterDt(page, "User Created on:").text()); 22 | String currentEmail = page.selectFirst("div#email-change") 23 | .selectFirst("label[for=current_email] + div").text(); 24 | 25 | return new AccountInfo(username, userId, registrationDate, avatarUrl, userClass, currentEmail); 26 | } 27 | 28 | private Date parseRegistrationDate(String dateString) { 29 | try { 30 | return REGISTRATION_DATE_FORMAT.parse(dateString); 31 | } catch (ParseException e) { 32 | throw new WebScrapeException("Cannot parser registration date"); 33 | } 34 | } 35 | 36 | private Element getAfterDt(Document page, String textOfDt) { 37 | return page.select("dt").stream() 38 | .filter(e -> e.text().equals(textOfDt)).findAny() 39 | .orElseThrow(() -> new WebScrapeException("Cannot find
" + textOfDt + "
")) 40 | .nextElementSibling(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/DeleteCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import org.jsoup.nodes.Document; 4 | 5 | public class DeleteCsrfTokenParser implements Parser { 6 | @Override 7 | public String parsePage(Document page, boolean isSukebei) { 8 | return ParseUtils.getCsrfToken(page.selectFirst("div.panel-danger")); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/EditCommentCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.CannotEditException; 4 | import de.kaysubs.tracker.nyaasi.exception.NoSuchCommentException; 5 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 6 | import org.jsoup.nodes.Document; 7 | import org.jsoup.nodes.Element; 8 | import org.jsoup.select.Elements; 9 | 10 | public class EditCommentCsrfTokenParser implements Parser { 11 | private final int commentId; 12 | 13 | public EditCommentCsrfTokenParser(int commentId) { 14 | this.commentId = commentId; 15 | } 16 | 17 | @Override 18 | public String parsePage(Document page, boolean isSukebei) { 19 | Element commentDiv = page.select("div#comments") 20 | .select("div.comment-panel").stream() 21 | .filter(e -> e.selectFirst("div#torrent-comment" + commentId) != null).findFirst() 22 | .orElseThrow(() -> new NoSuchCommentException()); 23 | 24 | if(commentDiv.selectFirst("button[title=Edit]") == null) { 25 | throw new CannotEditException(); 26 | } 27 | 28 | Elements crsfToken = commentDiv 29 | .select(".edit-comment-box") 30 | .select("input#csrf_token"); 31 | 32 | if(crsfToken.hasAttr("value")) { 33 | return crsfToken.attr("value"); 34 | } else { 35 | throw new WebScrapeException("Cannot extract CSRF token"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/EditTorrentParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.model.EditTorrentRequest; 4 | import org.jsoup.Connection; 5 | import org.jsoup.nodes.Document; 6 | import org.jsoup.nodes.FormElement; 7 | 8 | import java.util.List; 9 | 10 | public class EditTorrentParser implements Parser { 11 | @Override 12 | public EditTorrentRequest parsePage(Document page, boolean isSukebei) { 13 | List form = ((FormElement)page.selectFirst("form[method=POST][enctype=multipart/form-data]")).formData(); 14 | 15 | return new EditTorrentRequest( 16 | ParseUtils.getFormValue(form, "csrf_token"), 17 | ParseUtils.getFormValue(form, "display_name"), 18 | ParseUtils.parseSubCategory(ParseUtils.getFormValue(form, "category"), false, isSukebei), 19 | ParseUtils.getFormValue(form, "information"), 20 | ParseUtils.getFormValue(form, "description"), 21 | ParseUtils.contains(form, "is_anonymous", "y"), 22 | ParseUtils.contains(form, "is_hidden", "y"), 23 | ParseUtils.contains(form, "is_remake", "y"), 24 | ParseUtils.contains(form, "is_complete", "y") 25 | ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/LoginCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import org.jsoup.nodes.Document; 4 | 5 | public class LoginCsrfTokenParser implements Parser { 6 | @Override 7 | public String parsePage(Document page, boolean isSukebei) { 8 | return ParseUtils.getCsrfToken(page); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/ParseUtils.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.model.MainCategory; 4 | import de.kaysubs.tracker.nyaasi.model.SubCategory; 5 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 6 | import de.kaysubs.tracker.nyaasi.model.DataSize; 7 | import org.jsoup.Connection; 8 | import org.jsoup.nodes.Element; 9 | import org.jsoup.select.Elements; 10 | 11 | import java.math.BigDecimal; 12 | import java.util.Date; 13 | import java.util.List; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | public class ParseUtils { 18 | 19 | private final static Pattern CATEGORY_URL_PATTERN = Pattern.compile("/\\?c=([0-9])_([0-9])"); 20 | private final static Pattern CATEGORY_ID_PATTERN = Pattern.compile("([0-9])_([0-9])"); 21 | 22 | public static DataSize parseDataSize(String string) { 23 | String[] split = string.split(" "); 24 | if(split.length != 2) 25 | throw new WebScrapeException("Cannot parse data size"); 26 | 27 | DataSize.DataUnit unit = parseDataUnit(split[1]); 28 | 29 | BigDecimal size = new BigDecimal(split[0]); 30 | 31 | while(size.stripTrailingZeros().scale() > 0) { 32 | if(unit == DataSize.DataUnit.BYTE) 33 | throw new WebScrapeException("Cannot parse fractional byte size " + string); 34 | 35 | unit = DataSize.DataUnit.values()[unit.ordinal() - 1]; 36 | size = size.movePointRight(3); 37 | } 38 | 39 | return new DataSize(size.intValueExact(), unit); 40 | } 41 | 42 | private static DataSize.DataUnit parseDataUnit(String unitName) { 43 | for(DataSize.DataUnit unit : DataSize.DataUnit.values()) 44 | if(unit.getUnitName().equalsIgnoreCase(unitName)) 45 | return unit; 46 | 47 | throw new WebScrapeException("Unknown unit size \"" + unitName + "\""); 48 | } 49 | 50 | public static Date parseTimeStamp(String timestamp) { 51 | return new Date(Long.parseLong(timestamp) * 1000L); 52 | } 53 | 54 | public static SubCategory parseSubCategory(String url, boolean isUrl, boolean isSukebei) { 55 | Pattern pattern = isUrl ? CATEGORY_URL_PATTERN : CATEGORY_ID_PATTERN; 56 | Matcher matcher = pattern.matcher(url); 57 | 58 | if(matcher.matches()) { 59 | int categoryId = Integer.parseInt(matcher.group(1)); 60 | int subcategoryId = Integer.parseInt(matcher.group(2)); 61 | 62 | MainCategory mainCategory = isSukebei ? 63 | MainCategory.Sukebei.fromId(categoryId) : 64 | MainCategory.Nyaa.fromId(categoryId); 65 | 66 | return mainCategory.getSubcategoryFromId(subcategoryId); 67 | } else { 68 | throw new WebScrapeException("Cannot parse category url"); 69 | } 70 | } 71 | 72 | public static String getFormValue(List vals, String key) { 73 | return vals.stream() 74 | .filter(val -> val.key().equals(key)).findFirst() 75 | .orElseThrow(() -> new IllegalArgumentException("Expected form value for key \"" + key + "\"")) 76 | .value(); 77 | } 78 | 79 | public static boolean contains(List vals, String key, String value) { 80 | return vals.stream().anyMatch(val -> 81 | val.key().equals(key) && 82 | val.value().equals(value)); 83 | } 84 | 85 | public static String getCsrfToken(Element e) { 86 | Elements csrfToken = e 87 | .select("form[method=POST]") 88 | .select("input#csrf_token"); 89 | 90 | if(csrfToken.hasAttr("value")) { 91 | return csrfToken.attr("value"); 92 | } else { 93 | throw new WebScrapeException("Cannot extract csrf token"); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/Parser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import org.jsoup.nodes.Document; 4 | 5 | public interface Parser { 6 | 7 | T parsePage(Document page, boolean isSukebei); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/TorrentInfoParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.model.SubCategory; 4 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 5 | import de.kaysubs.tracker.nyaasi.model.*; 6 | import org.jsoup.nodes.Document; 7 | import org.jsoup.nodes.Element; 8 | 9 | import java.net.MalformedURLException; 10 | import java.net.URI; 11 | import java.net.URISyntaxException; 12 | import java.net.URL; 13 | import java.util.Date; 14 | import java.util.Optional; 15 | import java.util.regex.Matcher; 16 | import java.util.regex.Pattern; 17 | 18 | public class TorrentInfoParser implements Parser { 19 | private final static Pattern COMMENT_DIV_ID_PATTERN = Pattern.compile("torrent-comment([0-9]+)"); 20 | 21 | public static int parseCommentDivId(String divId) { 22 | Matcher matcher = COMMENT_DIV_ID_PATTERN.matcher(divId); 23 | if(matcher.matches()) { 24 | return Integer.parseInt(matcher.group(1)); 25 | } else { 26 | throw new WebScrapeException("Cannot parse comment ID"); 27 | } 28 | } 29 | 30 | @Override 31 | public TorrentInfo parsePage(Document page, boolean isSukebei) { 32 | TorrentInfo info = new TorrentInfo(); 33 | 34 | parseMainPanel(page, info, isSukebei); 35 | parseFileList(page, info); 36 | parseDescription(page, info); 37 | parseComments(page, info); 38 | 39 | return info; 40 | } 41 | 42 | private void parseMainPanel(Document page, TorrentInfo info, boolean isSukebei) { 43 | // select first/only panel with footer 44 | Element panel = page.select("div.panel").stream() 45 | .filter(e -> e.selectFirst("div.panel-footer.clearfix") != null) 46 | .findFirst().get(); 47 | 48 | TorrentState torrentState; 49 | if(panel.is(".panel-danger")) 50 | torrentState = TorrentState.REMAKE; 51 | else if(panel.is(".panel-success")) 52 | torrentState = TorrentState.TRUSTED; 53 | else 54 | torrentState = TorrentState.NORMAL; 55 | 56 | String title = panel.selectFirst("div.panel-heading") 57 | .selectFirst(".panel-title") 58 | .text(); 59 | 60 | Element body = panel.selectFirst("div.panel-body"); 61 | SubCategory category = ParseUtils.parseSubCategory( 62 | getCell(body, 0, 0) 63 | .select("a").get(1) 64 | .attr("href"), 65 | true, isSukebei); 66 | 67 | Optional uploader = Optional.ofNullable(getCell(body, 1, 0).selectFirst("a")).map(Element::text); 68 | String information = getCell(body, 2, 0).text(); 69 | DataSize size = ParseUtils.parseDataSize(getCell(body, 3, 0).text()); 70 | 71 | Date date = ParseUtils.parseTimeStamp(getCell(body, 0, 1).attr("data-timestamp")); 72 | int seeders = Integer.parseInt(getCell(body, 1, 1).selectFirst("span").text()); 73 | int leechers = Integer.parseInt(getCell(body, 2, 1).selectFirst("span").text()); 74 | int completed = Integer.parseInt( 75 | getCell(body, 3, 1).text()); 76 | String hash = getCell(body, 4, 0).selectFirst("kbd").text(); 77 | 78 | Element footer = panel.selectFirst("div.panel-footer.clearfix"); 79 | String downloadLink = footer.select("a[href^=/download/]").attr("href"); 80 | String magnetLink = footer.select("a.card-footer-item").attr("href"); 81 | 82 | info.setTitle(title); 83 | info.setCategory(category); 84 | info.setUploader(uploader); 85 | info.setTorrentState(torrentState); 86 | info.setInformation(information); 87 | info.setSize(size); 88 | info.setDate(date); 89 | info.setSeeders(seeders); 90 | info.setLeechers(leechers); 91 | info.setCompleted(completed); 92 | info.setHash(hash); 93 | 94 | try { 95 | URL baseUrl = new URL(isSukebei ? "https://sukebei.nyaa.si/" : "https://nyaa.si/"); 96 | info.setDownloadLink(new URL(baseUrl, downloadLink)); 97 | info.setMagnetLink(new URI(magnetLink)); 98 | } catch (MalformedURLException e) { 99 | throw new WebScrapeException("Cannot parse download url"); 100 | } catch (URISyntaxException e) { 101 | throw new WebScrapeException("Cannot parse magnet uri"); 102 | } 103 | } 104 | 105 | private Element getCell(Element panelBody, int row, int col) { 106 | return panelBody 107 | .select("div.row").get(row) 108 | .select("div.col-md-5").get(col); 109 | } 110 | 111 | private void parseComments(Document page, TorrentInfo info) { 112 | info.setComments(page.selectFirst("div#comments") 113 | .select("div.comment-panel").stream() 114 | .map(this::parseComment) 115 | .toArray(TorrentInfo.Comment[]::new)); 116 | } 117 | 118 | private TorrentInfo.Comment parseComment(Element commentPanel) { 119 | Element userLink = commentPanel.selectFirst("a[href^=/user/]"); 120 | 121 | String username = userLink.text(); 122 | boolean isTrusted = userLink.attr("title").equals("Trusted"); 123 | String avatar = commentPanel.selectFirst("img.avatar").attr("src"); 124 | Date date = ParseUtils.parseTimeStamp(commentPanel.selectFirst("small[data-timestamp]") 125 | .attr("data-timestamp")); 126 | 127 | Element commentDiv = commentPanel.selectFirst("div.comment-content"); 128 | int torrentId = parseCommentDivId(commentDiv.attr("id")); 129 | 130 | return new TorrentInfo.Comment(torrentId, username, isTrusted, avatar, date, commentDiv); 131 | } 132 | 133 | private void parseFileList(Document page, TorrentInfo info) { 134 | Element node = page.selectFirst("div.torrent-file-list > ul > li"); 135 | info.setFile(parseFileNode(node)); 136 | } 137 | 138 | private TorrentInfo.FileNode parseFileNode(Element li) { 139 | Element folderLink = li.selectFirst("a.folder"); 140 | 141 | if(folderLink == null) { 142 | String name = li.textNodes().get(0).text().trim(); 143 | 144 | String sizeText = li.selectFirst("span.file-size").text(); 145 | sizeText = sizeText.substring(1, sizeText.length() - 1); // trim brackets 146 | DataSize size = ParseUtils.parseDataSize(sizeText); 147 | 148 | return new TorrentInfo.File(name, size); 149 | } else { 150 | String name = folderLink.text(); 151 | TorrentInfo.FileNode[] children = Optional.ofNullable(li.selectFirst("ul")) 152 | .map(e -> e.children().stream() 153 | .map(this::parseFileNode) 154 | .toArray(TorrentInfo.FileNode[]::new)) 155 | .orElse(new TorrentInfo.FileNode[0]); 156 | 157 | return new TorrentInfo.Folder(name, children); 158 | } 159 | } 160 | 161 | private void parseDescription(Document page, TorrentInfo info) { 162 | Element descriptionDiv = page.selectFirst("div#torrent-description"); 163 | info.setDescriptionDiv(descriptionDiv); 164 | } 165 | 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/TorrentListPage.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 4 | import de.kaysubs.tracker.nyaasi.model.DataSize; 5 | import de.kaysubs.tracker.nyaasi.model.SubCategory; 6 | import de.kaysubs.tracker.nyaasi.model.TorrentPreview; 7 | import de.kaysubs.tracker.nyaasi.model.TorrentState; 8 | import org.jsoup.nodes.Document; 9 | import org.jsoup.nodes.Element; 10 | import org.jsoup.select.Elements; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URI; 14 | import java.net.URISyntaxException; 15 | import java.net.URL; 16 | import java.util.Date; 17 | import java.util.Optional; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | public class TorrentListPage implements Parser { 22 | 23 | @Override 24 | public TorrentPreview[] parsePage(Document page, boolean isSukebei) { 25 | return page.selectFirst("table.torrent-list") 26 | .selectFirst("tbody") 27 | .select("tr").stream() 28 | .map(e -> parseTorrent(e, isSukebei)) 29 | .toArray(TorrentPreview[]::new); 30 | } 31 | 32 | private TorrentPreview parseTorrent(Element row, boolean isSukebei) { 33 | TorrentState torrentState; 34 | if(row.is(".danger")) 35 | torrentState = TorrentState.REMAKE; 36 | else if(row.is(".success")) 37 | torrentState = TorrentState.TRUSTED; 38 | else 39 | torrentState = TorrentState.NORMAL; 40 | 41 | Element[] cells = row.select("td").toArray(new Element[0]); 42 | 43 | SubCategory category = ParseUtils.parseSubCategory(cells[0].select("a").attr("href"),true, isSukebei); 44 | 45 | Element titleCell = cells[1]; 46 | Element titleLink = titleCell.selectFirst("a:not(.comments)"); 47 | String title = titleLink.text(); 48 | int torrentId = parseViewUrl(titleLink.attr("href")); 49 | 50 | int commentCount = Optional.ofNullable(titleCell.selectFirst("a.comments")) 51 | .map(e -> Integer.parseInt(e.text())) 52 | .orElse(0); 53 | 54 | URL downloadLink; 55 | URI magnetLink; 56 | Elements links = cells[2].select("a"); 57 | try { 58 | URL baseUrl = new URL(isSukebei ? "https://sukebei.nyaa.si" : "https://nyaa.si"); 59 | downloadLink = new URL(baseUrl, links.get(0).attr("href")); 60 | 61 | magnetLink = new URI(links.get(1).attr("href")); 62 | } catch (MalformedURLException e) { 63 | throw new WebScrapeException("Cannot parse download url"); 64 | } catch (URISyntaxException e) { 65 | throw new WebScrapeException("Cannot parse magnet uri"); 66 | } 67 | 68 | DataSize size = ParseUtils.parseDataSize(cells[3].text()); 69 | Date date = ParseUtils.parseTimeStamp(cells[4].attr("data-timestamp")); 70 | int seeders = Integer.parseInt(cells[5].text()); 71 | int leechers = Integer.parseInt(cells[6].text()); 72 | int completed = Integer.parseInt(cells[7].text()); 73 | 74 | return new TorrentPreview(torrentId, torrentState, category, title, commentCount, downloadLink, magnetLink, size, date, seeders, leechers, completed); 75 | } 76 | 77 | Pattern VIEW_URL_PATTERN = Pattern.compile("/view/([0-9]+)"); 78 | private int parseViewUrl(String viewUrl) { 79 | Matcher matcher = VIEW_URL_PATTERN.matcher(viewUrl); 80 | if(matcher.matches()) { 81 | return Integer.parseInt(matcher.group(1)); 82 | } else { 83 | throw new WebScrapeException("Cannot parse view url"); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/UploadCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.CaptchaException; 4 | import org.jsoup.nodes.Document; 5 | 6 | public class UploadCsrfTokenParser implements Parser { 7 | @Override 8 | public String parsePage(Document page, boolean isSukebei) { 9 | if(page.selectFirst("div.g-recaptcha") != null) { 10 | throw new CaptchaException(); 11 | } 12 | 13 | return ParseUtils.getCsrfToken(page); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/ValidateDeleteComment.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 4 | import org.jsoup.nodes.Document; 5 | 6 | public class ValidateDeleteComment implements Parser { 7 | @Override 8 | public Void parsePage(Document page, boolean isSukebei) { 9 | boolean didSucceed = page.select("div.container") 10 | .select("div.alert-success").stream() 11 | .flatMap(div -> div.textNodes().stream()) 12 | .anyMatch(n -> n.text().trim().equals("Comment successfully deleted.")); 13 | 14 | if(didSucceed) { 15 | return null; 16 | } else { 17 | throw new WebScrapeException("Unknown Response"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/ValidateEmailChange.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.LoginException; 4 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 5 | import org.jsoup.nodes.Document; 6 | 7 | public class ValidateEmailChange implements Parser { 8 | @Override 9 | public Void parsePage(Document page, boolean isSukebei) { 10 | boolean wrongPassword = page.select("div.container") 11 | .select("div.alert-danger").stream() 12 | .anyMatch(e -> e.text().trim().equals("Incorrect password.")); 13 | 14 | boolean didSucceed = page.select("div.container") 15 | .select("div.alert-success > strong").stream() 16 | .anyMatch(strong -> strong.text().equals("Email successfully changed!")); 17 | 18 | if(wrongPassword)throw new LoginException(); 19 | else if(didSucceed)return null; 20 | else throw new WebScrapeException("Unknown response"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/ValidatePasswordChange.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.LoginException; 4 | import de.kaysubs.tracker.nyaasi.exception.PasswordLengthException; 5 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 6 | import org.jsoup.nodes.Document; 7 | import org.jsoup.nodes.Element; 8 | 9 | public class ValidatePasswordChange implements Parser { 10 | @Override 11 | public Void parsePage(Document page, boolean isSukebei) { 12 | Element helpBlock = page.selectFirst("div.help-block"); 13 | if(helpBlock != null) { 14 | if(helpBlock.text().startsWith("Password must be at least")) { 15 | throw new PasswordLengthException(); 16 | } else { 17 | throw new WebScrapeException(helpBlock.text().trim()); 18 | } 19 | } 20 | 21 | boolean wrongPassword = page.select("div.container") 22 | .select("div.alert-danger").stream() 23 | .anyMatch(e -> e.text().trim().equals("Incorrect password.")); 24 | 25 | boolean didSucceed = page.select("div.container") 26 | .select("div.alert-success > strong").stream() 27 | .anyMatch(strong -> strong.text().equals("Password successfully changed!")); 28 | 29 | if(wrongPassword)throw new LoginException(); 30 | else if(didSucceed)return null; 31 | else throw new WebScrapeException("Unknown response"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/ValidateUploadResponse.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.MissingTrackerException; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class ValidateUploadResponse implements Parser { 11 | private final Pattern TRACKER_ERROR_PATTERN = Pattern.compile("\\s*Please include (http://(nyaa|sukebei).tracker.wf:(7777|8888)/announce) in the trackers of the torrent\\s*"); 12 | 13 | @Override 14 | public Void parsePage(Document page, boolean isSukebei) { 15 | for(Element helpBlock : page.select("div.help-block")) { 16 | Matcher matcher = TRACKER_ERROR_PATTERN.matcher(helpBlock.text()); 17 | 18 | if(matcher.matches()) 19 | throw new MissingTrackerException(matcher.group(1)); 20 | } 21 | 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/WriteCommentCsrfTokenParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.CaptchaException; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | 7 | public class WriteCommentCsrfTokenParser implements Parser { 8 | @Override 9 | public String parsePage(Document page, boolean isSukebei) { 10 | Element form = page.selectFirst("form.comment-box"); 11 | 12 | if(form.selectFirst("div.g-recaptcha") == null) { 13 | return ParseUtils.getCsrfToken(form); 14 | } else { 15 | throw new CaptchaException(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/de/kaysubs/tracker/nyaasi/webscrape/WriteCommentResponseParser.java: -------------------------------------------------------------------------------- 1 | package de.kaysubs.tracker.nyaasi.webscrape; 2 | 3 | import de.kaysubs.tracker.nyaasi.exception.WebScrapeException; 4 | import org.jsoup.nodes.Document; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class WriteCommentResponseParser implements Parser { 10 | private final static Pattern REDIRECT_URL_PATTERN = 11 | Pattern.compile("https?://(?:sukebei\\.)?nyaa.si/view/(?:[0-9]+)#(.+)"); 12 | 13 | private final String commentDivId; 14 | 15 | public WriteCommentResponseParser(String redirectUrl) { 16 | Matcher matcher = REDIRECT_URL_PATTERN.matcher(redirectUrl); 17 | if(matcher.matches()) { 18 | commentDivId = matcher.group(1); 19 | } else { 20 | throw new WebScrapeException("Cannot parse redirectUrl"); 21 | } 22 | } 23 | 24 | @Override 25 | public Integer parsePage(Document page, boolean isSukebei) { 26 | boolean didSucceed = page.select("div.container") 27 | .select("div.alert-success").stream() 28 | .flatMap(div -> div.textNodes().stream()) 29 | .anyMatch(n -> n.text().trim().equals("Comment successfully posted.")); 30 | 31 | if(didSucceed) { 32 | return TorrentInfoParser.parseCommentDivId(page 33 | .selectFirst("#" + commentDivId) 34 | .selectFirst("div.comment-content") 35 | .attr("id")); 36 | } else { 37 | throw new WebScrapeException("Unknown response"); 38 | } 39 | } 40 | } 41 | --------------------------------------------------------------------------------