This commit is contained in:
dengqn 2025-09-25 19:53:41 +08:00
commit b80852e7a0
18 changed files with 3643 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

7
.idea/encodings.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

17
.idea/misc.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KotlinMultiplatform">
<allAvailableExecutables />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" project-jdk-name="25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

60
pom.xml Normal file
View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dengqn.app</groupId>
<artifactId>lingyin-api</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.21.2</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>25</source>
<target>25</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,23 @@
package com.dengqn.app.lingyinapi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
*
* @author dengqn
* @since 2025/9/23 18:45
*/
@Slf4j
@SpringBootApplication
public class Main {
void main(String[] args) {
try {
SpringApplication.run(Main.class, args);
} catch (Throwable e) {
log.info("error on app starting: {}", e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,62 @@
package com.dengqn.app.lingyinapi.apis;
import com.dengqn.app.lingyinapi.beans.SeedListItem;
import com.dengqn.app.lingyinapi.config.CookieConfig;
import com.dengqn.app.lingyinapi.html.HtmlTool;
import com.dengqn.app.lingyinapi.http.HttpTool;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 获取种子分页情况
*
* @author dengqn
* @since 2025/9/23 19:11
*/
@Slf4j
@RestController
@RequestMapping("/seed")
public class Seed {
private OkHttpClient okHttpClient;
public Seed(OkHttpClient okHttpClient) {
this.okHttpClient = okHttpClient;
}
@GetMapping("/list")
public ResponseEntity<List<SeedListItem>> getSeedPage(
@RequestParam(required = false, defaultValue = "0", name = "inclbookmarked") String inclbookmarked,
@RequestParam(required = false, defaultValue = "1", name = "incldead") String incldead,
@RequestParam(required = false, defaultValue = "0", name = "spstate") String spstate,
@RequestParam(required = false, defaultValue = "1", name = "page") String page
) throws IOException {
String url = "https://pt.soulvoice.club/special.php?inclbookmarked=%s&incldead=%s&spstate=%s&page=%s"
.formatted(inclbookmarked, incldead, spstate, page);
String html = HttpTool.getHTML(url, okHttpClient);
log.info("html:{}", html);
Document doc = Jsoup.parse(html);
List<SeedListItem> listItem = HtmlTool.getSeedListItem(doc);
return ResponseEntity.ok(listItem);
}
}

View File

@ -0,0 +1,11 @@
package com.dengqn.app.lingyinapi.beans;
/**
*
* @author dengqn
* @since 2025/9/25 18:22
*/
public record SeedListItem(String seedCover, String seedName, String seedUrl, boolean isFree, boolean isHitAndRun,
String downloadUrl, String time, String size, String seeders, String views,
boolean isAnonymous) {
}

View File

@ -0,0 +1,20 @@
package com.dengqn.app.lingyinapi.config;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
*
* @author dengqn
* @since 2025/9/23 19:10
*/
@Data
@Component
@ConfigurationProperties(prefix = "site.config.cookies")
public class CookieConfig {
private String secureLogin;
private String securePass;
private String secureUid;
}

View File

@ -0,0 +1,88 @@
package com.dengqn.app.lingyinapi.config;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;
import java.io.IOException;
import java.net.Socket;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
/**
*
* @author dengqn
* @since 2025/9/24 11:45
*/
@Slf4j
@Configuration
public class HttpClientConfig {
private CookieConfig cookieConfig;
public HttpClientConfig(CookieConfig cookieConfig) {
this.cookieConfig = cookieConfig;
}
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.cookieJar(new CookieJar() {
@Override
public void saveFromResponse(@NotNull HttpUrl httpUrl, @NotNull List<Cookie> list) {
}
@NotNull
@Override
public List<Cookie> loadForRequest(@NotNull HttpUrl httpUrl) {
return List.of(
new Cookie.Builder().domain("pt.soulvoice.club").name("c_secure_login").value(cookieConfig.getSecureLogin()).build(),
new Cookie.Builder().domain("pt.soulvoice.club").name("c_secure_uid").value(cookieConfig.getSecureUid()).build(),
new Cookie.Builder().domain("pt.soulvoice.club").name("c_secure_pass").value(cookieConfig.getSecurePass()).build()
);
}
})
.callTimeout(Duration.of(10, ChronoUnit.MINUTES))
.addInterceptor(new HTTPClientInterceptor())
.build();
}
class HTTPClientInterceptor implements Interceptor {
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
StringBuilder query = new StringBuilder();
query.append("\n------------------------\n");
query.append("URL:\n");
query.append("[").append(chain.request().method()).append("]-").append(chain.request().url()).append("\n");
Headers headers = chain.request().headers();
query.append("\nHEADER:\n");
for (String name : headers.names()) {
String value = headers.get(name);
query.append("| ").append(name).append(":").append(value).append("\n");
}
Optional.ofNullable(chain.request().body()).ifPresent(op -> {
try {
query.append("body length: ").append(op.contentLength()).append("\n");
} catch (IOException e) {
e.printStackTrace();
}
});
query.append("------------------------\n");
log.info(query.toString());
return chain.proceed(chain.request());
}
}
}

View File

@ -0,0 +1,65 @@
package com.dengqn.app.lingyinapi.html;
import com.dengqn.app.lingyinapi.beans.SeedListItem;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
*
* @author dengqn
* @since 2025/9/25 18:28
*/
@Slf4j
public class HtmlTool {
public static List<SeedListItem> getSeedListItem(Document doc) {
List<SeedListItem> items = new ArrayList<>();
Element torrents;
try {
torrents = doc.getElementsByClass("torrents").getFirst();
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<>();
}
return torrents.select("tr").stream().skip(1)
.map(tr -> {
try {
/**
* 标题部分
*/
Element torrentNames = tr.getElementsByTag("table").first();
Elements seedNames = torrentNames.getElementsByTag("td");
String seedCover = seedNames.get(0).getElementsByTag("img").first().attr("data-src");
String seedName = seedNames.get(1).getElementsByTag("a").first().attr("title");
String seedUrl = seedNames.get(1).getElementsByTag("a").first().attr("href");
boolean isFree = seedNames.get(1).getElementsByClass("pro_free").size() > 0;
boolean isHitAndRun = seedNames.get(1).getElementsByClass("hitandrun").size() > 0;
String downloadUrl = seedNames.get(3).getElementsByTag("a").first().attr("href");
/**
* 数据
*/
Elements tds = tr.selectXpath("td");
// skip 3
// 时间title属性
String time = tds.get(3).getElementsByTag("span").first().attr("title");
String size = tds.get(4).text().trim();
String seeders = tds.get(5).getElementsByTag("b").first().text().trim();
String views = tds.get(7).getElementsByTag("b") == null || tds.get(7).getElementsByTag("b").size() == 0 ? "0" : tds.get(7).getElementsByTag("b").first().text().trim();
boolean isAnonymous = tds.get(8).text().trim().contains("匿名");
return new SeedListItem(seedCover, seedName, seedUrl, isFree, isHitAndRun, downloadUrl, time, size, seeders, views, isAnonymous);
} catch (Exception e) {
log.error("解析报错跳过:" + e.getMessage());
return null;
}
}).filter(Objects::nonNull).toList();
}
}

View File

@ -0,0 +1,36 @@
package com.dengqn.app.lingyinapi.http;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpMethod;
import java.io.IOException;
/**
*
* @author dengqn
* @since 2025/9/24 14:23
*/
@Slf4j
public class HttpTool {
public static String getHTML(String url, OkHttpClient okHttpClient) {
try {
Response response = okHttpClient.newCall(new Request.Builder()
.url(url)
.method("GET", null)
.build())
.execute();
ResponseBody body = response.body();
if (body == null) return "";
byte[] bytes = body.bytes();
return new String(bytes);
} catch (IOException e) {
log.error(e.getLocalizedMessage(), e);
return "";
}
}
}

View File

@ -0,0 +1,6 @@
server.port=46578
server.servlet.context-path=/lingyin-api
site.config.cookies.secureLogin=bm9wZQ%3D%3D
site.config.cookies.securePass=69da36c4a07a6ce260aab777dc99b9d5
site.config.cookies.secureUid=MTQ5MTkw

View File

@ -0,0 +1 @@
spring.profiles.active=dev

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
package com.dengqn.app.lingyinapi.http;
import com.dengqn.app.lingyinapi.beans.SeedListItem;
import com.dengqn.app.lingyinapi.html.HtmlTool;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.List;
@Slf4j
@SpringBootTest
public class HttpToolTest {
@Test
public void testGetHtml() throws IOException {
String htmlData = new String(this.getClass().getResourceAsStream("/test-seed-list-page.html").readAllBytes());
// log.info("htmlData: {}", htmlData);
Document parse = Jsoup.parse(htmlData);
List<SeedListItem> listItem = HtmlTool.getSeedListItem(parse);
for (SeedListItem seedListItem : listItem) {
log.info("item-->{}", seedListItem);
}
}
}