1.2.2原版
This commit is contained in:
@@ -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 |
@@ -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();
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -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();
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
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 |
@@ -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 |
@@ -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}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user