1.2.2原版

This commit is contained in:
anian
2026-04-27 19:09:03 +08:00
commit 219938eb62
100 changed files with 14320 additions and 0 deletions
@@ -0,0 +1,39 @@
package la.moony.douban;
import org.springframework.stereotype.Component;
import org.springframework.util.PropertyPlaceholderHelper;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.processor.element.IElementModelStructureHandler;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.PluginContext;
import run.halo.app.theme.dialect.TemplateHeadProcessor;
import java.util.Properties;
@Component
public class ContactDoubanWidget implements TemplateHeadProcessor {
private final PropertyPlaceholderHelper
PROPERTY_PLACEHOLDER_HELPER = new PropertyPlaceholderHelper("${", "}");
private final PluginContext pluginContext;
public Mono<Void> process(ITemplateContext context, IModel model, IElementModelStructureHandler structureHandler) {
return Mono.just(this.contactFormHtml()).doOnNext((html) -> {
IModelFactory modelFactory = context.getModelFactory();
model.add(modelFactory.createText(html));
}).then();
}
private String contactFormHtml() {
Properties properties = new Properties();
properties.setProperty("version", pluginContext.getVersion());
properties.setProperty("pluginStaticPath", "/plugins/plugin-douban/assets/static");
return this.PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(
"<link href=\"${pluginStaticPath}/style.css?version=${version}\" rel=\"stylesheet\"/>\n "
+ "<script src=\"${pluginStaticPath}/contact-douban.iife.js?version=${version}\"></script>\n ", properties);
}
public ContactDoubanWidget(PluginContext pluginContext) {
this.pluginContext = pluginContext;
}
}
@@ -0,0 +1,125 @@
package la.moony.douban;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Objects;
import la.moony.douban.extension.CronDouban;
import la.moony.douban.service.DoubanService;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Component;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class CronDoubanReconciler implements Reconciler<Reconciler.Request>{
private static final Logger log = LoggerFactory.getLogger(CronDoubanReconciler.class);
private final ExtensionClient client;
private final DoubanService doubanService;
private Clock clock;
public CronDoubanReconciler(ExtensionClient client, DoubanService doubanService) {
this.client = client;
this.doubanService = doubanService;
this.clock = Clock.systemDefaultZone();
}
void setClock(Clock clock) {
this.clock = clock;
}
public Reconciler.Result reconcile(Reconciler.Request request) {
return (Reconciler.Result)this.client.fetch(CronDouban.class, request.name()).map((cronDouban) -> {
if (ExtensionUtil.isDeleted(cronDouban)) {
return Result.doNotRetry();
} else {
CronDouban.Spec spec = cronDouban.getSpec();
if (!spec.isSuspend()) {
return Result.doNotRetry();
}else {
String cron = spec.getCron();
String timezone = spec.getTimezone();
ZoneId zoneId = ZoneId.systemDefault();
if (timezone != null) {
try {
zoneId = (ZoneId) ApplicationConversionService.getSharedInstance().convert(timezone, ZoneId.class);
} catch (DateTimeException var18) {
log.error("Invalid zone ID {}", timezone, var18);
return Result.doNotRetry();
}
}
Instant now = Instant.now(this.clock);
if (!CronExpression.isValidExpression(cron)) {
log.error("Cron expression {} is invalid.", cron);
return Result.doNotRetry();
} else {
CronExpression cronExp = CronExpression.parse(cron);
CronDouban.Status status = cronDouban.getStatus();
Instant lastScheduledTimestamp = status.getLastScheduledTimestamp();
if (lastScheduledTimestamp == null) {
lastScheduledTimestamp = cronDouban.getMetadata().getCreationTimestamp();
}
ZonedDateTime nextFromNow = (ZonedDateTime)cronExp.next(now.atZone(zoneId));
ZonedDateTime nextFromLast = (ZonedDateTime)cronExp.next(lastScheduledTimestamp.atZone(zoneId));
if (nextFromNow != null && nextFromLast != null) {
if (Objects.equals(nextFromNow, nextFromLast)) {
log.info("Skip scheduling and next scheduled at {}", nextFromNow);
status.setNextSchedulingTimestamp(nextFromNow.toInstant());
this.client.update(cronDouban);
return new Reconciler.Result(true, Duration.between(now, nextFromNow));
} else {
this.doubanService.synchronizationDouban();
ZonedDateTime zonedNow = now.atZone(zoneId);
ZonedDateTime scheduleTimestamp = now.atZone(zoneId);
ZonedDateTime next;
for(next = lastScheduledTimestamp.atZone(zoneId); next != null && next.isBefore(zonedNow); next = (ZonedDateTime)cronExp.next(next)) {
scheduleTimestamp = next;
}
status.setLastScheduledTimestamp(scheduleTimestamp.toInstant());
if (next != null) {
status.setNextSchedulingTimestamp(next.toInstant());
}
this.client.update(cronDouban);
log.info("Scheduled at {} and next scheduled at {}", scheduleTimestamp, next);
return new Reconciler.Result(true, Duration.between(now, next));
}
} else {
return Result.doNotRetry();
}
}
}
}
}).orElseGet(Reconciler.Result::doNotRetry);
}
public Controller setupWith(ControllerBuilder builder) {
return builder.extension(new CronDouban()).workerCount(1).build();
}
}
@@ -0,0 +1,101 @@
package la.moony.douban;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import la.moony.douban.extension.DoubanMovie;
import la.moony.douban.service.DoubanService;
import la.moony.douban.vo.DoubanMovieVo;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
@Component
public class DoubanEndpoint implements CustomEndpoint {
private final String doubanMovieTag = "api.douban.moony.la/v1alpha1/DoubanMovie";
private final DoubanService doubanService;
public DoubanEndpoint(DoubanService doubanService) {
this.doubanService = doubanService;
}
@Override
public RouterFunction<ServerResponse> endpoint() {
return SpringdocRouteBuilder.route()
.GET("doubanmovies", this::listDoubanmovie, builder -> {
builder.operationId("listDoubanMovie")
.description("List doubanMovie.")
.tag(doubanMovieTag)
.response(
responseBuilder()
.implementation(ListResult.generateGenericClass(DoubanMovie.class))
);
DoubanMovieQuery.buildParameters(builder);
})
.GET("doubanmovies/-/genres", this::ListGenres,
builder -> builder.operationId("ListGenres")
.description("List all douban genres.")
.tag(doubanMovieTag)
.parameter(parameterBuilder()
.name("type")
.in(ParameterIn.QUERY)
.description("Genres type to query")
.required(false)
.implementation(String.class)
)
.response(responseBuilder()
.implementation(String.class)
))
.GET("doubanmovies/-/getDoubanDetail", this::getDoubanDetail,
builder -> builder.operationId("getDoubanDetail")
.description("getDoubanDetail.")
.tag(doubanMovieTag)
.parameter(parameterBuilder()
.name("url")
.in(ParameterIn.QUERY)
.description("doubanmovie url to query")
.required(false)
.implementation(String.class)
)
.response(responseBuilder()
.implementation(DoubanMovieVo.class)
))
.build();
}
Mono<ServerResponse> listDoubanmovie(ServerRequest request) {
DoubanMovieQuery query = new DoubanMovieQuery(request);
return doubanService.listDoubanMovie(query)
.flatMap(doubanMovies -> ServerResponse.ok().bodyValue(doubanMovies));
}
private Mono<ServerResponse> ListGenres(ServerRequest request) {
String type = request.queryParam("type").orElse(null);
return doubanService.listAllGenres(type)
.collectList()
.flatMap(result -> ServerResponse.ok().bodyValue(result));
}
private Mono<ServerResponse> getDoubanDetail(ServerRequest request) {
String url = request.queryParam("url").orElse(null);
return doubanService.getDoubanDetail(url)
.flatMap(result -> ServerResponse.ok().bodyValue(result));
}
@Override
public GroupVersion groupVersion() {
return GroupVersion.parseAPIVersion("api.douban.moony.la/v1alpha1");
}
}
@@ -0,0 +1,163 @@
package la.moony.douban;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import la.moony.douban.extension.DoubanMovie;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.server.ServerRequest;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.SortableRequest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import static java.util.Comparator.comparing;
import static org.springdoc.core.fn.builders.arrayschema.Builder.arraySchemaBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.in;
import static run.halo.app.extension.router.QueryParamBuildUtil.sortParameter;
public class DoubanMovieQuery extends SortableRequest {
public DoubanMovieQuery(ServerRequest request) {
super(request.exchange());
}
@Nullable
public String getKeyword() {
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
}
@Nullable
public String getStatus() {
return queryParams.getFirst("status");
}
@Nullable
public String getType() {
return queryParams.getFirst("type");
}
@Nullable
public String getDataType() {
return queryParams.getFirst("dataType");
}
@Nullable
public Optional<List<String>> getGenres() {
return Optional.ofNullable(queryParams.get("genre"))
.filter(genres -> !genres.isEmpty());
}
public ListOptions toListOptions() {
var builder = ListOptions.builder(super.toListOptions());
Optional.ofNullable(getKeyword())
.filter(StringUtils::isNotBlank)
.ifPresent(keyword -> builder.andQuery(
contains("spec.name", getKeyword()))
);
Optional.ofNullable(getStatus())
.filter(StringUtils::isNotBlank)
.ifPresent(status -> builder.andQuery(equal("faves.status", status)));
Optional.ofNullable(getType())
.filter(StringUtils::isNotBlank)
.ifPresent(type -> builder.andQuery(equal("spec.type", type)));
Optional.ofNullable(getDataType())
.filter(StringUtils::isNotBlank)
.ifPresent(dataType -> builder.andQuery(equal("spec.dataType", dataType)));
getGenres().ifPresent(genres -> builder.andQuery(in("spec.genres", genres)));
return builder.build();
}
public Comparator<DoubanMovie> toComparator() {
List<Comparator<DoubanMovie>> comparators = new ArrayList<>();
var sort = getSort();
var ctOrder = sort.getOrderFor("createTime");
if (ctOrder != null) {
Comparator<DoubanMovie> comparator =
comparing(doubanMovie -> doubanMovie.getFaves().getCreateTime());
if (ctOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}
Comparator<DoubanMovie> comparator =
comparing(doubanMovie -> doubanMovie.getFaves().getCreateTime());
comparators.add(comparator.reversed());
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
}
public Sort getSort() {
var sort = SortResolver.defaultInstance.resolve(exchange);
return sort.and(Sort.by("faves.createTime").descending());
}
public PageRequest toPageRequest() {
return PageRequestImpl.of(getPage(), getSize(), getSort());
}
public static void buildParameters(Builder builder) {
IListRequest.buildParameters(builder);
builder.parameter(sortParameter())
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("keyword")
.description("DoubanMovies filtered by keyword.")
.implementation(String.class)
.required(false))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("status")
.description("DoubanMovies filtered by status.")
.implementation(String.class)
.required(false))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("type")
.description("DoubanMovies filtered by type.")
.implementation(String.class)
.required(false))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("dataType")
.description("DoubanMovies filtered by dataType.")
.implementation(String.class)
.required(false))
.parameter(parameterBuilder()
.in(ParameterIn.QUERY)
.name("genre")
.description("DoubanMovies filtered by genre.")
.required(false)
.array(
arraySchemaBuilder()
.uniqueItems(true)
.schema(schemaBuilder()
.implementation(String.class))
)
.implementationArray(String.class));
}
}
@@ -0,0 +1,82 @@
package la.moony.douban;
import la.moony.douban.extension.CronDouban;
import la.moony.douban.extension.DoubanMovie;
import org.springframework.stereotype.Component;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.index.IndexSpec;
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
import java.util.Set;
import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute;
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;
/**
* @author moony
* @url https://moony.la
* @date 2024/2/1
*/
@Component
public class DoubanPlugin extends BasePlugin {
private final SchemeManager schemeManager;
public DoubanPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
super(pluginContext);
this.schemeManager = schemeManager;
}
@Override
public void start() {
schemeManager.register(DoubanMovie.class, indexSpecs -> {
indexSpecs.add(new IndexSpec()
.setName("spec.genres")
.setIndexFunc(multiValueAttribute(DoubanMovie.class, doubanMovie -> {
var genres = doubanMovie.getSpec().getGenres();
return genres == null ? Set.of() : Set.copyOf(genres);
}))
);
indexSpecs.add(new IndexSpec()
.setName("spec.type")
.setIndexFunc(
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getType())));
indexSpecs.add(new IndexSpec()
.setName("spec.dataType")
.setIndexFunc(
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getDataType())));
indexSpecs.add(new IndexSpec()
.setName("spec.name")
.setIndexFunc(
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getName())));
indexSpecs.add(new IndexSpec()
.setName("spec.id")
.setIndexFunc(
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getId())));
indexSpecs.add(new IndexSpec()
.setName("faves.status")
.setIndexFunc(simpleAttribute(DoubanMovie.class, doubanMovie -> {
var status = doubanMovie.getFaves().getStatus();
return status == null ? null : status.toString();
}))
);
indexSpecs.add(new IndexSpec()
.setName("faves.createTime")
.setIndexFunc(simpleAttribute(DoubanMovie.class, moment -> {
var createTime = moment.getFaves().getCreateTime();
return createTime == null ? null : createTime.toString();
}))
);
});
schemeManager.register(CronDouban.class);
}
@Override
public void stop() {
schemeManager.unregister(Scheme.buildFromType(DoubanMovie.class));
schemeManager.unregister(Scheme.buildFromType(CronDouban.class));
}
}
@@ -0,0 +1,22 @@
package la.moony.douban;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class DoubanRequest {
private String userId;
private Integer count;
private Integer start;
private DoubanType type;
private DoubanStatus status;
public enum DoubanType{
movie,music,book,game,drama;
}
public enum DoubanStatus{
mark,doing,done;
}
}
@@ -0,0 +1,56 @@
package la.moony.douban;
import la.moony.douban.finders.DoubanFinder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.plugin.ReactiveSettingFetcher;
import run.halo.app.theme.TemplateNameResolver;
@Configuration
@RequiredArgsConstructor
@Slf4j
public class DoubanRouter {
private final TemplateNameResolver templateNameResolver;
private final ReactiveSettingFetcher settingFetcher;
private final DoubanFinder doubanFinder;
@Bean
RouterFunction<ServerResponse> friendTemplateRoute() {
return RouterFunctions.route().GET("/douban",this::handlerFunction)
.build();
}
private Mono<ServerResponse> handlerFunction(ServerRequest request) {
String type = request.queryParam("type")
.filter(StringUtils::isNotBlank)
.orElse(null);
String status = request.queryParam("status")
.filter(StringUtils::isNotBlank)
.orElse("done");
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "douban")
.flatMap( templateName -> ServerResponse.ok().render(templateName,
java.util.Map.of("title",getDoubanTitle(),
"douban",doubanFinder.list(type,status),
"genres",doubanFinder.listAllGenres(type),
"types",doubanFinder.listAllType())));
}
Mono<String> getDoubanTitle() {
return this.settingFetcher.get("base").map(
setting -> setting.get("title").asText("豆瓣记录")).defaultIfEmpty(
"豆瓣记录");
}
}
@@ -0,0 +1,42 @@
package la.moony.douban.controller;
import la.moony.douban.extension.DoubanMovie;
import la.moony.douban.service.DoubanService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.plugin.ApiVersion;
@ApiVersion("v1alpha1")
@RequestMapping("/douban")
@RestController
@Slf4j
public class DoubanController {
private final DoubanService doubanService;
private final ReactiveExtensionClient client;
public DoubanController(DoubanService doubanService, ReactiveExtensionClient client) {
this.doubanService = doubanService;
this.client = client;
}
@PostMapping("/synchronizationDouban")
public void synchronizationDouban() {
doubanService.synchronizationDouban();
}
@DeleteMapping("/clear")
public Mono<Void> clearLogs() {
return client.list(DoubanMovie.class, null, null).flatMap(doubanMovie -> client.delete(doubanMovie))
.then();
}
}
@@ -0,0 +1,42 @@
package la.moony.douban.extension;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import java.time.Instant;
@Data
@ToString
@EqualsAndHashCode(callSuper = true)
@GVK(
group = "douban.moony.la",
version = "v1alpha1",
kind = "CronDouban",
singular = "crondouban",
plural = "crondoubans"
)
public class CronDouban extends AbstractExtension {
private Spec spec = new Spec();
private Status status = new Status();
@Data
public static class Spec {
private String cron;
private String timezone;
private boolean suspend;
}
@Data
public static class Status {
private Instant lastScheduledTimestamp;
private Instant nextSchedulingTimestamp;
}
}
@@ -0,0 +1,59 @@
package la.moony.douban.extension;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import java.time.Instant;
import java.util.Set;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
@Data
@ToString
@EqualsAndHashCode(callSuper = true)
@GVK(kind = "DoubanMovie", group = "douban.moony.la",
version = "v1alpha1", singular = "doubanmovie", plural = "doubanmovies")
public class DoubanMovie extends AbstractExtension {
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";
@Schema(requiredMode = REQUIRED)
private DoubanMovieSpec spec;
@Schema(requiredMode = REQUIRED)
private DoubanMovieFaves faves;
@Data
public static class DoubanMovieSpec {
private String name;
private String poster;
private String link;
private String id;
private String score;
private String year;
private String type;
private String pubdate;
private String cardSubtitle;
private String dataType;
private Set<String> genres;
}
@Data
public static class DoubanMovieFaves {
private String remark;
private Instant createTime;
private String score;
private String status;
}
}
@@ -0,0 +1,29 @@
package la.moony.douban.finders;
import la.moony.douban.vo.DoubanGenresVo;
import la.moony.douban.vo.DoubanMovieVo;
import la.moony.douban.vo.DoubanTypeVo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
public interface DoubanFinder {
Flux<DoubanGenresVo> listAllGenres(String type);
Flux<DoubanTypeVo> listAllType();
Mono<ListResult<DoubanMovieVo>> list(Integer page, Integer size);
Flux<DoubanMovieVo> listByGenre(String genre);
Mono<DoubanMovieVo> get(String doubanName);
Mono<ListResult<DoubanMovieVo>> listByType(Integer page, Integer size, String type);
Mono<ListResult<DoubanMovieVo>> list(Integer page, Integer size, String type, String status);
Flux<DoubanMovieVo> listByType(String type);
Flux<DoubanMovieVo> list(String type,String status);
}
@@ -0,0 +1,242 @@
package la.moony.douban.finders.impl;
import jakarta.annotation.Nonnull;
import la.moony.douban.extension.DoubanMovie;
import la.moony.douban.finders.DoubanFinder;
import la.moony.douban.vo.DoubanGenresVo;
import la.moony.douban.vo.DoubanMovieVo;
import la.moony.douban.vo.DoubanTypeVo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.theme.finders.Finder;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import static run.halo.app.extension.index.query.QueryFactory.all;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNotNull;
@Finder("doubanFinder")
@RequiredArgsConstructor
public class DoubanFinderImpl implements DoubanFinder {
private final ReactiveExtensionClient client;
public static final Predicate<DoubanMovie> FIXED_PREDICATE = doubanMovie -> true;
@Override
public Mono<ListResult<DoubanMovieVo>> list(Integer page, Integer size) {
var pageRequest = PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());
return pageDoubanMovie(null, pageRequest);
}
@Override
public Flux<DoubanMovieVo> listByGenre(String genre) {
var listOptions = new ListOptions();
var query = and(isNotNull("faves.status"),equal("spec.genres", genre));
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listAll(DoubanMovie.class, listOptions, defaultSort())
.flatMap(this::getDoubanMovieVo);
}
@Override
public Mono<DoubanMovieVo> get(String doubanName) {
return client.get(DoubanMovie.class, doubanName)
.filter(FIXED_PREDICATE)
.flatMap(this::getDoubanMovieVo);
}
static Sort defaultSort() {
return Sort.by("faves.createTime").descending();
}
@Override
public Flux<DoubanGenresVo> listAllGenres(String type) {
var listOptions = new ListOptions();
var query = and(all("spec.genres"),isNotNull("faves.status"));
FieldSelector fieldSelector = FieldSelector.of(query);
if (StringUtils.isNotEmpty(type)) {
fieldSelector = fieldSelector.andQuery(equal("spec.type", type));
}
listOptions.setFieldSelector(fieldSelector);
return client.listAll(DoubanMovie.class, listOptions, defaultSort())
.flatMapIterable(doubanMovie -> {
var genres = doubanMovie.getSpec().getGenres();
if (genres == null) {
return List.of();
}
return genres.stream()
.map(genre -> new DoubanMoviePair(genre, doubanMovie.getMetadata().getName()))
.toList();
})
.groupBy(DoubanMoviePair::genresName)
.flatMap(groupedFlux -> groupedFlux.count()
.defaultIfEmpty(0L)
.map(count -> DoubanGenresVo.builder()
.name(groupedFlux.key())
.doubanCount(count.intValue())
.build()
)
);
}
record DoubanMoviePair(String genresName, String doubanMovieName) {
}
@Override
public Flux<DoubanTypeVo> listAllType() {
var listOptions = new ListOptions();
var query = and(all("spec.type"),isNotNull("faves.status"));
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listAll(DoubanMovie.class, listOptions, defaultSort())
.flatMapIterable(doubanMovie -> {
var type = doubanMovie.getSpec().getType();
Set<String> types = new HashSet<>();
if (type == null) {
return List.of();
}else {
types.add(doubanMovie.getSpec().getType());
}
return types.stream()
.map(typeName -> new DoubanMoviePair(typeName, doubanMovie.getMetadata().getName()))
.toList();
})
.groupBy(DoubanMoviePair::genresName)
.flatMap(groupedFlux -> groupedFlux.count()
.defaultIfEmpty(0L)
.map(count -> DoubanTypeVo.builder()
.name(getTypeName(groupedFlux.key()))
.key(groupedFlux.key())
.doubanCount(count.intValue())
.build()
)
);
}
public String getTypeName(String type){
String name = "";
switch (type){
case "movie":
name = "电影";
break;
case "book":
name = "图书";
break;
case "music":
name = "音乐";
break;
case "game":
name = "游戏";
break;
case "drama":
name = "舞台剧";
break;
}
return name;
}
@Override
public Mono<ListResult<DoubanMovieVo>> listByType(Integer page, Integer size, String typeName) {
var query = all();
if (org.apache.commons.lang3.StringUtils.isNoneBlank(typeName)) {
query = and(query, equal("spec.type", typeName));
}
var pageRequest =
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());
return pageDoubanMovie(FieldSelector.of(query), pageRequest);
}
@Override
public Mono<ListResult<DoubanMovieVo>> list(Integer page, Integer size, String typeName, String statusName) {
var query = all();
if (org.apache.commons.lang3.StringUtils.isNoneBlank(typeName)) {
query = and(query, equal("spec.type", typeName));
}
if (org.apache.commons.lang3.StringUtils.isNoneBlank(statusName)) {
query = and(query, equal("faves.status", statusName));
}
var pageRequest =
PageRequestImpl.of(pageNullSafe(page), sizeNullSafe(size), defaultSort());
return pageDoubanMovie(FieldSelector.of(query), pageRequest);
}
@Override
public Flux<DoubanMovieVo> listByType(String type) {
var listOptions = new ListOptions();
var query = and(equal("faves.status", "done"),equal("spec.type", type));
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listAll(DoubanMovie.class, listOptions, defaultSort())
.flatMap(this::getDoubanMovieVo);
}
@Override
public Flux<DoubanMovieVo> list(String type, String status) {
var listOptions = new ListOptions();
var query = all();
if (org.apache.commons.lang3.StringUtils.isNoneBlank(type)) {
if (org.apache.commons.lang3.StringUtils.isNoneBlank(type)){
query = and(query,equal("spec.type", type));
}else {
query = and(equal("faves.status", "done"),equal("spec.type", type));
}
}
if (org.apache.commons.lang3.StringUtils.isNoneBlank(status)) {
query = and(query, equal("faves.status", status));
}
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listAll(DoubanMovie.class, listOptions, defaultSort())
.flatMap(this::getDoubanMovieVo);
}
private Mono<ListResult<DoubanMovieVo>> pageDoubanMovie(FieldSelector fieldSelector, PageRequest page) {
var listOptions = new ListOptions();
var query = isNotNull("faves.status");
if (fieldSelector != null && fieldSelector.query() != null) {
query = and(query, fieldSelector.query());
}
listOptions.setFieldSelector(FieldSelector.of(query));
return client.listBy(DoubanMovie.class, listOptions, page)
.flatMap(list -> Flux.fromStream(list.get())
.concatMap(this::getDoubanMovieVo)
.collectList()
.map(doubanMovieVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(), doubanMovieVos)
)
)
.defaultIfEmpty(
new ListResult<>(page.getPageNumber(), page.getPageSize(), 0L, List.of()));
}
private Mono<DoubanMovieVo> getDoubanMovieVo(@Nonnull DoubanMovie doubanMovie) {
DoubanMovieVo doubanMovieVo = DoubanMovieVo.from(doubanMovie);
return Mono.just(doubanMovieVo);
}
int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1);
}
int sizeNullSafe(Integer size) {
return ObjectUtils.defaultIfNull(size, 10);
}
}
@@ -0,0 +1,21 @@
package la.moony.douban.service;
import com.fasterxml.jackson.databind.node.ArrayNode;
import la.moony.douban.DoubanMovieQuery;
import la.moony.douban.DoubanRequest;
import la.moony.douban.extension.DoubanMovie;
import la.moony.douban.vo.DoubanMovieVo;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult;
public interface DoubanService {
void synchronizationDouban();
Mono<DoubanMovieVo> getDoubanDetail(String url);
Flux<String> listAllGenres(String type);
Mono<ListResult<DoubanMovie>> listDoubanMovie(DoubanMovieQuery doubanMovieQuery);
}
@@ -0,0 +1,530 @@
package la.moony.douban.service.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.Nonnull;
import la.moony.douban.DoubanMovieQuery;
import la.moony.douban.extension.DoubanMovie;
import la.moony.douban.service.DoubanService;
import la.moony.douban.vo.DoubanMovieVo;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.plugin.ReactiveSettingFetcher;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
@Component
public class DoubanServiceImpl implements DoubanService {
private final Logger log = LoggerFactory.getLogger(DoubanServiceImpl.class);
private final ReactiveExtensionClient reactiveClient;
private static final String DB_API_LIST_URL = "https://fatesinger.com/dbapi/user/{userid}/interests?count={count}&start={start}&type={type}&status={status}";
private static final String DB_API_LIST_URL1 = "https://fatesinger.com/dbapi/user/%s/interests?count=%s&start=%s&type=%s&status=%s";
private static final String DB_API_DETAIL_URL = "https://fatesinger.com/dbapi/{type}/{id}?ck=xgtY&for_mobile=1";
private static final String TMDB_API_URL = "https://hk.fatesinger.com/api/{type}/{tmdbId}?api_key={apiKey}&language=zh-CN";
private final ReactiveSettingFetcher settingFetcher;
public DoubanServiceImpl(ReactiveExtensionClient reactiveClient,
ReactiveSettingFetcher settingFetcher) {
this.reactiveClient = reactiveClient;
this.settingFetcher = settingFetcher;
}
@Override
public void synchronizationDouban() {
getDoubanId().flatMap(doubanId->{
if (StringUtils.isNotEmpty(doubanId)){
addDouban(doubanId);
}
return Mono.empty();
}).subscribe();
}
public void addDouban(String DOUBAN_ID){
String[] types = {"movie","music","book", "game", "drama"};
String[] status = {"done","doing","mark"};
log.info("豆瓣开始抓取数据");
for (String type : types) {
for (String s : status) {
AtomicBoolean confition = new AtomicBoolean(true);
AtomicInteger i = new AtomicInteger(0);
while (confition.get()) {
String baseUrl = String.format(DB_API_LIST_URL1,DOUBAN_ID, 49, 49 * i.get() , type, s);
ArrayNode arrayNode = listDouban(baseUrl);
if (arrayNode.isEmpty()){
confition.set(false);
}else {
for(JsonNode node : arrayNode){
JsonNode subject = node.get("subject");
String name = subject.get("title").asText();
String poster = subject.get("pic").get("large").asText();
String id = subject.get("id").asText();
String doubanScore = subject.get("rating").get("value").asText();
String link = subject.get("url").asText();
String year = "";
if (subject.get("year")!=null){
year =subject.get("year").asText();
}
String type1 = type;
String pubdate = "";
if (subject.get("pubdate").isArray() && subject.get("pubdate").size()>0){
pubdate = subject.get("pubdate").get(0).asText("");
}
String cardSubtitle = subject.get("card_subtitle").asText();
Set<String> genres = new HashSet();
if (subject.get("genres")!=null){
subject.get("genres").forEach(genre -> {
genres.add(genre.asText());
});
}
String createTime = node.get("create_time").asText();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 定义日期格式
Date date = null;
try {
date = dateFormat.parse(createTime); // 将字符串转换为Date类型
} catch (ParseException e) {
throw new RuntimeException(e);
}
String remark = node.get("comment").asText();
String score;
if (!node.get("rating").isEmpty()){
score = node.get("rating").get("value").asText();
} else {
score = "";
}
String status1 = node.get("status").asText();
var listOptions = new ListOptions();
var query = and(equal("spec.type", type1),equal("spec.id", id));
listOptions.setFieldSelector(FieldSelector.of(query));
Flux<DoubanMovie> list = reactiveClient.listAll(DoubanMovie.class, listOptions, Sort.by("faves.createTime"));
Mono<Boolean> booleanMono = list.hasElements();
Date finalDate = date;
String finalScore = score;
String finalYear = year;
String finalPubdate = pubdate;
Date finalDate1 = date;
booleanMono.flatMap(hasValue -> {
if (hasValue) {
return list.next().flatMap(doubanMovie -> {
if (StringUtils.isNotEmpty(doubanMovie.getFaves().getStatus())) {
if (doubanMovie.getFaves().getStatus().equals(status1)) {
confition.set(false);
return Mono.empty();
}
}
doubanMovie.getFaves().setCreateTime(finalDate.toInstant());
doubanMovie.getFaves().setRemark(remark);
doubanMovie.getFaves().setScore(finalScore);
doubanMovie.getFaves().setStatus(status1);
return reactiveClient.update(doubanMovie);
});
} else {
DoubanMovie doubanMovie = new DoubanMovie();
doubanMovie.setMetadata(new Metadata());
doubanMovie.getMetadata().setGenerateName("douban-movie-");
doubanMovie.setSpec(new DoubanMovie.DoubanMovieSpec());
doubanMovie.getSpec().setName(name);
doubanMovie.getSpec().setPoster(poster);
doubanMovie.getSpec().setId(id);
doubanMovie.getSpec().setScore(doubanScore);
doubanMovie.getSpec().setLink(link);
doubanMovie.getSpec().setYear(finalYear);
doubanMovie.getSpec().setType(type1);
doubanMovie.getSpec().setPubdate(finalPubdate);
doubanMovie.getSpec().setCardSubtitle(cardSubtitle);
doubanMovie.getSpec().setGenres(genres);
doubanMovie.getSpec().setDataType("db");
doubanMovie.setFaves(new DoubanMovie.DoubanMovieFaves());
doubanMovie.getFaves().setCreateTime(finalDate1.toInstant());
doubanMovie.getFaves().setRemark(remark);
doubanMovie.getFaves().setScore(score);
doubanMovie.getFaves().setStatus(status1);
return reactiveClient.create(doubanMovie);
}
}).subscribe();
}
i.set(i.get()+1);
}
}
}
}
log.info("豆瓣结束抓取数据");
}
@Override
public Mono<DoubanMovieVo> getDoubanDetail(String url) {
DoubanMovie doubanMovie = new DoubanMovie();
Map<String, Object> matcher = matcher(url);
String type = (String) matcher.get("type");
String id = (String) matcher.get("id");
int index = (int) matcher.get("index");
if (StringUtils.isNotEmpty(type) && StringUtils.isNotEmpty(id)){
switch (index){
case 1:
return embedHandlerDoubanlist(type,id);
case 2:
return embedHandlerDoubanablum(type,id);
case 3:
return embedHandlerDoubandrama(type,id);
case 4:
return embedHandlerTheMovieDb(type,id);
}
return getDoubanMovieVo(doubanMovie);
}else {
return getDoubanMovieVo(doubanMovie);
}
}
@Override
public Flux<String> listAllGenres(String type) {
ListOptions listOptions = new ListOptions();
if (StringUtils.isNotEmpty(type)) {
listOptions.setFieldSelector(FieldSelector.of(equal("spec.type",type)));
}
return reactiveClient.listAll(DoubanMovie.class, listOptions,
Sort.by("metadata.name").descending())
.flatMapIterable(doubanMovie -> {
var genres = doubanMovie.getSpec().getGenres();
return Objects.requireNonNullElseGet(genres, List::of);
})
.distinct();
}
@Override
public Mono<ListResult<DoubanMovie>> listDoubanMovie(DoubanMovieQuery query) {
return this.settingFetcher.get("base").flatMap(base ->
reactiveClient.listBy(DoubanMovie.class, query.toListOptions(), query.toPageRequest())
.flatMap(listResult -> Flux.fromStream(listResult.get())
.map(doubanMovie -> {
//替换反代图片地址
String poster = doubanMovie.getSpec().getPoster();
boolean isProxy = base.get("isProxy").asBoolean(false);
if (doubanMovie.getSpec().getDataType() != "halo" && isProxy) {
if (StringUtils.isNotEmpty(base.get("proxyHost").asText())) {
String proxyHost = base.get("proxyHost").asText();
String replace =
poster.replaceAll("https://img\\d+.doubanio.com", proxyHost);
doubanMovie.getSpec().setPoster(replace);
}
}
return doubanMovie;
})
.collectList()
.map(list -> new ListResult<>(listResult.getPage(), listResult.getSize(),
listResult.getTotal(), list)
)));
}
public Mono<DoubanMovieVo> embedHandlerDoubanlist(String type,String id){
if (StringUtils.contains("movie,book,music", type)){
return doubanDetail(type,id);
}
return getDoubanMovieVo(new DoubanMovie());
}
public Mono<DoubanMovieVo> embedHandlerDoubanablum(String type,String id){
if (StringUtils.contains("game", type)){
return doubanDetail(type,id);
}
return getDoubanMovieVo(new DoubanMovie());
}
public Mono<DoubanMovieVo> embedHandlerDoubandrama(String type,String id){
if (StringUtils.contains("drama", type)){
return doubanDetail(type,id);
}
return getDoubanMovieVo(new DoubanMovie());
}
public Mono<DoubanMovieVo> embedHandlerTheMovieDb(String type,String id){
if (StringUtils.contains("tv,movie", type)){
return getApiKey().flatMap(apiKey->{
if (StringUtils.isNotEmpty(apiKey) ){
return tmdbDetail(type,id,apiKey);
}
return getDoubanMovieVo(new DoubanMovie());
});
}
return getDoubanMovieVo(new DoubanMovie());
}
public Mono<DoubanMovieVo> tmdbDetail(String type,String id,String apiKey){
var listOptions = new ListOptions();
var query = and(equal("spec.type", type),equal("spec.id", id),equal("spec.dataType", "tmdb"));
listOptions.setFieldSelector(FieldSelector.of(query));
Flux<DoubanMovie> list = reactiveClient.listAll(DoubanMovie.class, listOptions, Sort.by("faves.createTime"));
Mono<Boolean> booleanMono = list.hasElements();
return booleanMono.flatMap(hasValue ->{
if (hasValue){
return (Mono<DoubanMovieVo>) list.next().flatMap(doubanMovie -> {
return getDoubanMovieVo(doubanMovie);
});
}else {
DoubanMovie doubanMovieDetail = new DoubanMovie();
return tmdbDetailRequest(type, id,apiKey).flatMap(jsonNode->{
String name = jsonNode.get("title")!=null ? jsonNode.get("title").asText() : jsonNode.get("name").asText();
String poster = "https://image.tmdb.org/t/p/original"+jsonNode.get("poster_path").asText();
String tmdbId = jsonNode.get("id").asText();
String doubanScore = jsonNode.get("vote_average").asText();
String link = jsonNode.get("homepage").asText();
String year = "";
String pubdate = jsonNode.get("release_date")!=null ? jsonNode.get("release_date").asText() : jsonNode.get("first_air_date").asText();
String cardSubtitle = jsonNode.get("overview").asText();
Set<String> genres = new HashSet();
if (jsonNode.get("genres")!=null){
jsonNode.get("genres").forEach(genre -> {
genres.add(genre.get("name").asText());
});
}
doubanMovieDetail.setMetadata(new Metadata());
doubanMovieDetail.getMetadata().setGenerateName("douban-movie-");
doubanMovieDetail.setSpec(new DoubanMovie.DoubanMovieSpec());
doubanMovieDetail.getSpec().setName(name);
doubanMovieDetail.getSpec().setPoster(poster);
doubanMovieDetail.getSpec().setId(tmdbId);
doubanMovieDetail.getSpec().setScore(doubanScore);
doubanMovieDetail.getSpec().setLink(link);
doubanMovieDetail.getSpec().setYear(year);
doubanMovieDetail.getSpec().setType(type);
doubanMovieDetail.getSpec().setPubdate(pubdate);
doubanMovieDetail.getSpec().setCardSubtitle(cardSubtitle);
doubanMovieDetail.getSpec().setGenres(genres);
doubanMovieDetail.getSpec().setDataType("tmdb");
doubanMovieDetail.setFaves(new DoubanMovie.DoubanMovieFaves());
doubanMovieDetail.getFaves().setCreateTime(Instant.now());
doubanMovieDetail.getFaves().setRemark(null);
doubanMovieDetail.getFaves().setScore(null);
doubanMovieDetail.getFaves().setStatus(null);
return reactiveClient.create(doubanMovieDetail)
.thenReturn(doubanMovieDetail)
.flatMap(doubanMovie -> getDoubanMovieVo(doubanMovie));
}).onErrorResume(WebClientResponseException.NotFound.class, error -> {
log.error("Resource not found: ",error.getMessage());
return getDoubanMovieVo(doubanMovieDetail);
});
}
});
}
public Mono<DoubanMovieVo> doubanDetail(String type,String id){
var listOptions = new ListOptions();
var query = and(equal("spec.type", type),equal("spec.id", id));
listOptions.setFieldSelector(FieldSelector.of(query));
Flux<DoubanMovie> list = reactiveClient.listAll(DoubanMovie.class, listOptions, Sort.by("faves.createTime"));
Mono<Boolean> booleanMono = list.hasElements();
return this.settingFetcher.get("base").flatMap(base ->booleanMono
.flatMap(hasValue ->{
if (hasValue){
return (Mono<DoubanMovieVo>) list.next().flatMap(doubanMovie -> {
return getDoubanMovieVo(doubanMovie);
});
}else {
DoubanMovie doubanMovieDetail = new DoubanMovie();
return doubanDetailRequest(type, id).flatMap(jsonNode->{
String name = jsonNode.get("title").asText();
String poster = jsonNode.get("pic").get("large").asText();
String doubanId = jsonNode.get("id").asText();
String doubanScore = jsonNode.get("rating").get("value").asText();
String link = jsonNode.get("url").asText();
String year = "";
if (jsonNode.get("year")!=null){
year =jsonNode.get("year").asText();
}
String pubdate = "";
if (jsonNode.get("pubdate")!=null){
if (jsonNode.get("pubdate").isArray() && jsonNode.get("pubdate").size()>0){
pubdate = jsonNode.get("pubdate").get(0).asText("");
}
}
String cardSubtitle = jsonNode.get("card_subtitle").asText();
Set<String> genres = new HashSet();
if (jsonNode.get("genres")!=null){
jsonNode.get("genres").forEach(genre -> {
genres.add(genre.asText());
});
}
doubanMovieDetail.setMetadata(new Metadata());
doubanMovieDetail.getMetadata().setGenerateName("douban-movie-");
doubanMovieDetail.setSpec(new DoubanMovie.DoubanMovieSpec());
doubanMovieDetail.getSpec().setName(name);
doubanMovieDetail.getSpec().setPoster(poster);
doubanMovieDetail.getSpec().setId(doubanId);
doubanMovieDetail.getSpec().setScore(doubanScore);
doubanMovieDetail.getSpec().setLink(link);
doubanMovieDetail.getSpec().setYear(year);
doubanMovieDetail.getSpec().setType(type);
doubanMovieDetail.getSpec().setPubdate(pubdate);
doubanMovieDetail.getSpec().setCardSubtitle(cardSubtitle);
doubanMovieDetail.getSpec().setGenres(genres);
doubanMovieDetail.getSpec().setDataType("db");
doubanMovieDetail.setFaves(new DoubanMovie.DoubanMovieFaves());
doubanMovieDetail.getFaves().setCreateTime(Instant.now());
doubanMovieDetail.getFaves().setRemark(null);
doubanMovieDetail.getFaves().setScore(null);
doubanMovieDetail.getFaves().setStatus(null);
return reactiveClient.create(doubanMovieDetail)
.thenReturn(doubanMovieDetail)
.flatMap(doubanMovie -> getDoubanMovieVo(doubanMovie));
}).onErrorResume(WebClientResponseException.NotFound.class, error -> {
log.error("Resource not found: ",error.getMessage());
return getDoubanMovieVo(doubanMovieDetail);
});
}
}).map(doubanMovie -> {
//替换反代图片地址
String poster = doubanMovie.getSpec().getPoster();
boolean isProxy = base.get("isProxy").asBoolean(false);
if (doubanMovie.getSpec().getDataType() != "halo" && isProxy) {
if (StringUtils.isNotEmpty(base.get("proxyHost").asText())) {
String proxyHost = base.get("proxyHost").asText();
String replace =
poster.replaceAll("https://img\\d+.doubanio.com", proxyHost);
doubanMovie.getSpec().setPoster(replace);
}
}
return doubanMovie;
}));
}
public Map<String,Object> matcher(String url){
Map<String,Object> map = new HashMap<>();
String[] patterns = {
"https?://(\\w+)\\.douban\\.com/subject/(\\d+)",
"https?://www\\.douban\\.com/(\\w+)/(\\d+)",
"https?://www\\.douban\\.com/location/(\\w+)/(\\d+)",
"https?://www\\.themoviedb\\.org/(\\w+)/(\\d+)"
};
String input = url;
int index = 0;
for (String regex : patterns) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
String type = null;
String id = null;
index = index+1;
if (matcher.find()) {
type = matcher.group(1);
id = matcher.group(2);
map.put("type",type);
map.put("id",id);
map.put("index",index);
return map;
}else {
log.info("No match found {}",url);
}
map.put("type",type);
map.put("id",id);
map.put("index",index);
}
return map;
}
Mono<String> getDoubanId() {
return this.settingFetcher.get("base")
.map(setting -> setting.get("doubanId").asText());
}
Mono<String> getApiKey() {
return this.settingFetcher.get("base")
.map(setting -> setting.get("apiKey").asText());
}
private Mono<DoubanMovieVo> getDoubanMovieVo(@Nonnull DoubanMovie doubanMovie) {
DoubanMovieVo doubanMovieVo = DoubanMovieVo.from(doubanMovie);
return Mono.just(doubanMovieVo);
}
public ArrayNode listDouban(String url){
String result = null;
ArrayNode jsonNodes = new ArrayNode(null);
try {
// 创建连接
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// 设置请求方法
connection.setRequestMethod("GET");
// 获取响应结果
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
result = response.toString();
// 创建一个 ObjectMapper 实例
ObjectMapper objectMapper = new ObjectMapper();
// 将 String 解析为 JsonNode
JsonNode jsonNode = objectMapper.readTree(result);
// 将 JsonNode 转换为 ObjectNode
if (jsonNode.isObject()) {
ObjectNode objectNode = (ObjectNode) jsonNode;
jsonNodes = objectNode.withArray("/interests");
}
} catch (Exception e) {
e.printStackTrace();
}
return jsonNodes;
}
public Mono<JsonNode> doubanDetailRequest(String type,String id) {
return WebClient.create().get()
.uri(DB_API_DETAIL_URL, type,id)
.retrieve()
.bodyToMono(JsonNode.class);
}
public Mono<JsonNode> tmdbDetailRequest(String type,String tmdbId,String apiKey) {
return WebClient.create().get()
.uri(TMDB_API_URL, type,tmdbId,apiKey)
.retrieve()
.bodyToMono(JsonNode.class);
}
}
@@ -0,0 +1,12 @@
package la.moony.douban.vo;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class DoubanGenresVo {
String name;
Integer doubanCount;
}
@@ -0,0 +1,32 @@
package la.moony.douban.vo;
import la.moony.douban.extension.DoubanMovie;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import org.springframework.util.Assert;
import run.halo.app.extension.MetadataOperator;
@Data
@SuperBuilder
@ToString
@EqualsAndHashCode
public class DoubanMovieVo {
private MetadataOperator metadata;
private DoubanMovie.DoubanMovieSpec spec;
private DoubanMovie.DoubanMovieFaves faves;
public static DoubanMovieVo from(DoubanMovie doubanMovie) {
Assert.notNull(doubanMovie, "The doubanMovie must not be null.");
return DoubanMovieVo.builder()
.metadata(doubanMovie.getMetadata())
.spec(doubanMovie.getSpec())
.faves(doubanMovie.getFaves())
.build();
}
}
@@ -0,0 +1,15 @@
package la.moony.douban.vo;
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class DoubanTypeVo {
String name;
String key;
Integer doubanCount;
}
@@ -0,0 +1,8 @@
apiVersion: plugin.halo.run/v1alpha1
kind: ReverseProxy
metadata:
name: douban-static-assets
rules:
- path: /static/**
file:
directory: static
@@ -0,0 +1,54 @@
apiVersion: v1alpha1
kind: Role
metadata:
name: role-template-douban-anonymous
labels:
rbac.authorization.halo.run/aggregate-to-anonymous: "true"
halo.run/role-template: "true"
halo.run/hidden: "true"
annotations:
rbac.authorization.halo.run/display-name: "查看朋友圈提交模板"
rules:
- apiGroups: [ "api.douban.moony.la" ]
resources: [ "doubanmovies","doubanmovies/genres","doubanmovies/getDoubanDetail"]
verbs: [ "get", "list" ]
---
apiVersion: v1alpha1
kind: Role
metadata:
name: role-template-douban-view
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/module: "豆瓣"
rbac.authorization.halo.run/display-name: "豆瓣查看"
rbac.authorization.halo.run/ui-permissions: |
["plugin:douban:view"]
rules:
- apiGroups: [ "douban.moony.la" ]
resources: [ "doubanmovies"]
verbs: [ "get", "list" ]
---
apiVersion: v1alpha1
kind: Role
metadata:
name: role-template-douban-manage
labels:
halo.run/role-template: "true"
annotations:
rbac.authorization.halo.run/module: "豆瓣"
rbac.authorization.halo.run/display-name: "豆瓣管理"
rbac.authorization.halo.run/ui-permissions: |
["plugin:douban:manage"]
rbac.authorization.halo.run/dependencies: |
["role-template-douban-view"]
rules:
- apiGroups: [ "douban.moony.la" ]
resources: [ "doubanmovies"]
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
- nonResourceURLs: [ "/apis/api.plugin.halo.run/v1alpha1/plugins/plugin-douban/douban/*"]
verbs: [ "create" , "delete" ]
@@ -0,0 +1,39 @@
apiVersion: v1alpha1
kind: Setting
metadata:
name: plugin-douban-settings
spec:
forms:
- group: base
label: 基本设置
formSchema:
- $formkit: text
label: 页面标题
name: title
validation: required
value: '豆瓣记录'
- $formkit: text
label: 豆瓣的ID
name: doubanId
help: 我的豆瓣 https://www.douban.com/people/xxxx xxxx 就是你的ID
- $formkit: text
label: TMDB API Key
name: apiKey
help: 设置TMDB API Key在https://www.themoviedb.org/settings/api 自行申请
- $formkit: radio
label: 启用图片代理
name: isProxy
id: isProxy
value: false
help: 如果用豆瓣源的图片很慢可以自己搭建给图片反代站点
options:
- value: true
label: 开启
- value: false
label: 关闭
- $formkit: text
if: "$get(isProxy).value === true"
label: 代理地址
name: proxyHost
value: ""
help: "搭建教程:https://docs.kunkunyu.com/docs/plugin-douban/use#%E9%85%8D%E7%BD%AE%E5%9B%BE%E7%89%87%E4%BB%A3%E7%90%86"
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+23
View File
@@ -0,0 +1,23 @@
apiVersion: plugin.halo.run/v1alpha1
kind: Plugin
metadata:
name: plugin-douban
annotations:
"store.halo.run/app-id": "app-srBOL"
spec:
enabled: true
requires: ">=2.20.0"
author:
name: 困困鱼
website: https://github.com/chengzhongxue
logo: logo.png
homepage: https://www.halo.run/store/apps/app-srBOL
repo: https://github.com/chengzhongxue/plugin-douban
issues: https://github.com/chengzhongxue/plugin-douban/issues
settingName: plugin-douban-settings
configMapName: plugin-douban-configmap
displayName: "豆瓣"
description: "提供从豆瓣爬取到的数据,提供页面路由"
license:
- name: "GPL-3.0"
url: "https://github.com/halo-dev/plugin-douban/blob/main/LICENSE"
@@ -0,0 +1,62 @@
(function () {
function getDoubanDetail(src, e) {
var url = '/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail?url=' + src;
fetch(url)
.then(response => response.json())
.then(data => {
renderer(data, e);
})
.catch(console.error)
}
function renderer(data, e) {
let img = data.spec.dataType == 'db' ? `${data.spec.poster}` : data.spec.poster
let date = new Date(data.faves.createTime);
let dateString = date.toLocaleString(); // 使用默认的日期和时间格式
const r = document.createElement("div");
r.classList.add('doulist-item')
r.innerHTML = `<div class="doulist-subject">
<div class="doulist-post"><img decoding="async" referrerpolicy="no-referrer" class="fade-before fade-after"
src="${img}"></div>
<div class="db--viewTime JiEun">Marked ${dateString}</div>
<div class="doulist-content">
<div class="doulist-title"><a class="cute" target="_blank" rel="external nofollow"
href="${data.spec.link}">${data.spec.name}</a></div>
<div class="rating"><span class="allstardark"><span class="allstarlight" style="width: ${data.spec.score * 10}%;"></span></span><span
class="rating_nums">${data.spec.score}</span></div>
<div class="abstract">${data.faves?.remark != null && data.faves?.remark != '' ? data.faves?.remark : data.spec.cardSubtitle}</div>
</div>
</div>`
e.appendChild(r);
}
const mf = () => {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const e = entry.target;
const src = e.getAttribute("src");
if (src != null && src != '') {
getDoubanDetail(src, e);
observer.unobserve(e); // 停止观察已经加载的元素,防止重复加载
}
}
});
});
document.querySelectorAll("douban").forEach((e) => {
observer.observe(e);
});
};
document.addEventListener("DOMContentLoaded", () => {
mf();
}, {
once: true
});
document.addEventListener("pjax:success", () => {
mf();
});
})();
+297
View File
@@ -0,0 +1,297 @@
// @ts-nocheck
class HALO_DOUBAN {
constructor() {
this.ver = "1.2.2";
this.type = "movie";
this.status = "done";
this.finished = false;
this.paged = 1;
this.genre_list = [];
this.genre = [];
this.subjects = [];
this._create();
}
on(t, e, n) {
var a = document.querySelectorAll(e);
a.forEach((item) => {
item.addEventListener(t, n);
});
}
_addSearchParams(url, params = {}) {
url = new URL(url, window.location.origin);
let new_url = new URL(
`${url.origin}${url.pathname}?${new URLSearchParams([
...Array.from(url.searchParams.entries()),
...Object.entries(params),
])}`
);
return new_url.href;
}
_fetchGenres() {
document.querySelector(".db--genres").innerHTML = "";
const url = '/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/genres';
fetch(
this._addSearchParams(url, {
type: this.type,
})
)
.then((response) => response.json())
.then((data) => {
if (data.length) {
this.genre_list = data;
this._renderGenre();
}
});
return true;
}
_statusChange() {
this.on("click", ".db--typeItem", (t) => {
const self = t.currentTarget;
if (self.classList.contains("is-active")) {
// const index = this.genre.indexOf(self.innerText);
return;
}
document.querySelector(".db--list").innerHTML = "";
document.querySelector(".lds-ripple").classList.remove("u-hide");
document
.querySelector(".db--typeItem.is-active")
.classList.remove("is-active");
self.classList.add("is-active");
this.status = self.dataset.status;
this.paged = 1;
this.finished = false;
this.subjects = [];
this._fetchData();
return;
});
}
_handleGenreClick() {
this.on("click", ".db--genreItem", (t) => {
const self = t.currentTarget;
if (self.classList.contains("is-active")) {
const index = this.genre.indexOf(self.innerText);
self.classList.remove("is-active");
this.genre.splice(index, 1);
this.paged = 1;
this.finished = false;
this.subjects = [];
this._fetchData();
return;
}
document.querySelector(".db--list").innerHTML = "";
document.querySelector(".lds-ripple").classList.remove("u-hide");
self.classList.add("is-active");
this.genre.push(self.innerText);
this.paged = 1;
this.finished = false;
this.subjects = [];
this._fetchData();
return;
});
}
_renderGenre() {
document.querySelector(".db--genres").innerHTML = this.genre_list
.map((item) => {
return `<span class="db--genreItem">${item}</span>`;
})
.join("");
this._handleGenreClick();
}
_fetchData() {
var url = `/apis/api.douban.moony.la/v1alpha1/doubanmovies?page=${this.paged}&size=49&type=${this.type}&status=${this.status}`;
const genre = this.genre
if (genre.length > 0) {
for (let i = 0; i < genre.length; i++) {
url = url + `&genre=${genre[i]}`
}
}
fetch(url)
.then((response) => response.json())
.then((data) => {
if (data.items.length) {
if(this.subjects==null){
this.subjects =[]
}
if (
document
.querySelector(".db--list")
.classList.contains("db--list__card")
) {
this.subjects = [...this.subjects, ...data.items];
this._randerDateTemplate();
} else {
this.subjects = [...this.subjects, ...data.items];
this._randerListTemplate();
}
document
.querySelector(".lds-ripple")
.classList.add("u-hide");
} else {
document
.querySelector(".db--list")
.classList.contains("db--list__card")
? this._randerDateTemplate()
: this._randerListTemplate();
this.finished = true;
document
.querySelector(".lds-ripple")
.classList.add("u-hide");
}
});
}
_randerDateTemplate() {
if (!this.subjects.length)
return (document.querySelector(
".db--list"
).innerHTML = `<div class="db--empty"></div>`);
const result = this.subjects.reduce((result, item) => {
const date = new Date(item.faves.createTime);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const key = `${year}-${month.toString().padStart(2, "0")}`;
if (Object.prototype.hasOwnProperty.call(result, key)) {
result[key].push(item);
} else {
result[key] = [item];
}
return result;
}, {});
let html = ``;
for (let key in result) {
const date = key.split("-");
html += `<div class="db--listBydate"><div class="db--titleDate JiEun"><div class="db--titleDate__day">${date[1]}</div><div class="db--titleDate__month">${date[0]}</div></div><div class="db--dateList__card">`;
html += result[key]
.map((movie) => {
return `<div class="db--item"><img src="${
movie.spec.dataType == 'halo' ? movie.spec.poster : movie.spec.poster
}" referrerpolicy="unsafe-url" class="db--image"><div class="db--score JiEun">${
movie.spec.score > 0
? '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" ><path d="M12 20.1l5.82 3.682c1.066.675 2.37-.322 2.09-1.584l-1.543-6.926 5.146-4.667c.94-.85.435-2.465-.799-2.567l-6.773-.602L13.29.89a1.38 1.38 0 0 0-2.581 0l-2.65 6.53-6.774.602C.052 8.126-.453 9.74.486 10.59l5.147 4.666-1.542 6.926c-.28 1.262 1.023 2.26 2.09 1.585L12 20.099z"></path></svg>' +
movie.spec.score
: ""
}${
movie.spec.year > 0 ? " · " + movie.spec.year : ""
}</div><div class="db--title"><a href="${
movie.spec.link
}" target="_blank">${movie.spec.name}</a></div>
</div>`;
})
.join("");
html += `</div></div>`;
}
document.querySelector(".db--list").innerHTML = html;
}
_randerListTemplate() {
if (!this.subjects.length)
return (document.querySelector(
".db--list"
).innerHTML = `<div class="db--empty"></div>`);
document.querySelector(".db--list").innerHTML = this.subjects
.map((item) => {
return `<div class="db--item"><img src="${
item.spec.dataType == 'halo' ? item.spec.poster : item.spec.poster
}" referrerpolicy="unsafe-url" class="db--image"><div class="ipc-signpost JiEun">${
item.faves.createTime
}</div><div class="db--score JiEun">${
item.spec.score > 0
? '<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" ><path d="M12 20.1l5.82 3.682c1.066.675 2.37-.322 2.09-1.584l-1.543-6.926 5.146-4.667c.94-.85.435-2.465-.799-2.567l-6.773-.602L13.29.89a1.38 1.38 0 0 0-2.581 0l-2.65 6.53-6.774.602C.052 8.126-.453 9.74.486 10.59l5.147 4.666-1.542 6.926c-.28 1.262 1.023 2.26 2.09 1.585L12 20.099z"></path></svg>' +
item.spec.score
: ""
}${
item.spec.year > 0 ? " · " + item.spec.year : ""
}</div><div class="db--title"><a href="${
item.spec.link
}" target="_blank">${item.spec.name}</a></div>
</div>
</div>`;
})
.join("");
}
_handleScroll() {
window.addEventListener("scroll", () => {
var t = window.scrollY || window.pageYOffset;
// @ts-ignore
if (
document.querySelector(".block-more").offsetTop +
// @ts-ignore
-window.innerHeight <
t &&
document
.querySelector(".lds-ripple")
.classList.contains("u-hide") &&
!this.finished
) {
document
.querySelector(".lds-ripple")
.classList.remove("u-hide");
this.paged++;
this._fetchData();
}
});
}
_handleNavClick() {
this.on("click", ".db--navItem", (t) => {
if (t.target.classList.contains("current")) return;
this.genre = [];
this.type = t.target.dataset.type;
if (this.type != "book") {
this._fetchGenres();
document
.querySelector(".db--genres")
.classList.remove("u-hide");
} else {
document.querySelector(".db--genres").classList.add("u-hide");
}
document.querySelector(".db--list").innerHTML = "";
document.querySelector(".lds-ripple").classList.remove("u-hide");
document
.querySelector(".db--navItem.current")
.classList.remove("current");
const self = t.target;
self.classList.add("current");
this.paged = 1;
//this.status = "done";
this.finished = false;
this.subjects = [];
this._fetchData();
});
}
_create() {
if (document.querySelector(".db--container")) {
if (document.querySelector(".db--navItem.current")) {
this.type = document.querySelector(
".db--navItem.current"
).dataset.type;
}
if (document.querySelector(".db--list").dataset.type)
this.type = document.querySelector(".db--list").dataset.type;
if (this.type == "movie") {
document
.querySelector(".db--genres")
.classList.remove("u-hide");
}
this._fetchGenres();
this._fetchData();
this._handleScroll();
this._handleNavClick();
this._statusChange();
}
}
}
new HALO_DOUBAN();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1574920521513" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7948" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3-12.3 12.7-12.1 32.9 0.6 45.3l183.7 179.1-43.4 252.9c-1.2 6.9-0.1 14.1 3.2 20.3 8.2 15.6 27.6 21.7 43.2 13.4L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3z" p-id="7949" fill="#f99b01"></path></svg>

After

Width:  |  Height:  |  Size: 809 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1574920559231" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8164" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M908.1 353.1l-253.9-36.9L540.7 86.1c-3.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L369.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3-12.3 12.7-12.1 32.9 0.6 45.3l183.7 179.1-43.4 252.9c-1.2 6.9-0.1 14.1 3.2 20.3 8.2 15.6 27.6 21.7 43.2 13.4L512 754l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3zM664.8 561.6l36.1 210.3L512 672.7 323.1 772l36.1-210.3-152.8-149L417.6 382 512 190.7 606.4 382l211.2 30.7-152.8 148.9z" p-id="8165" fill="#f99b01"></path></svg>

After

Width:  |  Height:  |  Size: 927 B

+1
View File
@@ -0,0 +1 @@
:root{--db--text-color-light:rgba(0, 0, 0, 0.6)}.doulist-item{margin:30px 0;border:1px solid #eee;border-radius:4px}.doulist-item:hover{box-shadow:0 3px 12px rgba(0,0,0,.05)}.doulist-item .doulist-subject{display:flex;align-items:flex-start;line-height:1.6;padding:12px;position:relative}.doulist-item .doulist-subject .db--viewTime{position:absolute;right:0;top:0;background:#f5c518;color:#000;border-radius:4px 4px 0 4px;line-height:1;padding:3px 5px 3px 10px;font-size:12px;display:flex;margin-bottom:2px;font-weight:900}.doulist-item .doulist-subject .db--viewTime:after{content:"";border-top-left-radius:0;border-bottom-left-radius:0;left:0;top:0;margin-left:-.2rem;border-radius:0 4px 4px 4px;background:inherit;height:100%;position:absolute;width:.75rem;transform:skewX(20deg)}.doulist-item .doulist-subject .doulist-content{flex:1 1 auto}.doulist-item .doulist-subject .doulist-post{width:96px;margin-right:15px;display:flex;flex:0 0 auto}.doulist-item .doulist-subject .doulist-title{margin-bottom:5px;font-size:18px}.doulist-item .doulist-subject .doulist-title a{text-decoration:none!important}.doulist-item .doulist-subject .rating{margin:0 0 5px;font-size:14px;line-height:1;display:flex;align-items:center}.doulist-item .doulist-subject .rating .allstardark{position:relative;color:#f99b01;height:16px;width:80px;background-repeat:repeat;background-image:url(/plugins/plugin-douban/assets/static/img/star.svg);background-size:auto 100%;margin-right:5px}.doulist-item .doulist-subject .rating .allstarlight{position:absolute;left:0;color:#f99b01;height:16px;overflow:hidden;background-repeat:repeat;background-image:url(/plugins/plugin-douban/assets/static/img/star-fill.svg);background-size:auto 100%}.doulist-item .doulist-subject .abstract{font-size:14px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.6;max-height:3.2em}.doulist-item .doulist-subject img{width:96px!important;height:96px!important;border-radius:4px;object-fit:cover}
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="https://www.thymeleaf.org" th:with="version = '1.2.2'">
<head>
<title th:text="${title}"></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" id="wpd-css-css" th:href="@{/plugins/plugin-douban/assets/static/db.min.css?v={version}(version=${version})}" type="text/css"
media="screen">
<style>
h1,
h2,
h3,
h4,
h5 {
margin-top: 0;
margin-bottom: 0;
font-weight: 400;
}
.metabar--block {
pointer-events: auto;
display: flex;
align-items: center;
position: relative;
height: 68px;
font-size: 14px;
justify-content: center;
}
.layoutSingleColumn {
max-width: 720px;
margin-left: auto;
margin-right: auto;
width: 92%;
}
.layoutSingleColumn--wide {
max-width: 1180px;
}
.u-paddingTop50 {
padding-top: 50px
}
.db--title a {
color: rgb(0 0 0 / 80%);
text-decoration: none;
}
</style>
</head>
<body>
<header class="metabar metabar--bordered">
<div class="metabar--block" itemprop="publisher" itemscope="">
<h1 class="metabar--headline" itemprop="logo" itemscope="" th:text="${title}"></h1>
</div>
</header>
<main class="main">
<div class="layoutSingleColumn layoutSingleColumn--wide u-paddingTop50">
<section class="db--container">
<nav class="db--nav">
<div th:each="type : ${types}" class="db--navItem JiEun"
th:classappend="${type.key == 'movie' ? 'current' : ''}" th:data-type="${type.key}"
th:text="${type.key}"></div>
</nav>
<div class="db--genres"></div>
<div class="db--type">
<div class="db--typeItem" data-status="mark">想看</div>
<div class="db--typeItem" data-status="doing">在看</div>
<div class="db--typeItem is-active" data-status="done">看过</div>
</div>
<div class="db--list db--list__card" data-type="movie">
</div>
<div class="block-more block-more__centered">
<div class="lds-ripple u-hide">
</div>
</div>
</section>
</div>
</main>
<footer class="site--footer">
<script th:src="@{/plugins/plugin-douban/assets/static/db.min.js?v={version}(version=${version})}"></script>
</footer>
</body>
</html>