From 5cb2b7005bd2624669a01d7ab991c716cafbb2cb Mon Sep 17 00:00:00 2001 From: chuzhongzai Date: Sun, 24 Dec 2023 21:04:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E7=BA=BF=E5=8A=A8=E6=80=81=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=9B=BE=E7=89=87=E5=90=8E=E7=AB=AF,=E4=BD=BF?= =?UTF-8?q?=E7=94=A8mpvKey=E4=BB=A5=E5=8F=8AimgKey=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E9=93=BE=E6=8E=A5;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lionwebsite/Configuration/CustomBean.java | 2 +- .../Controller/GalleryManageController.java | 27 ++- .../Dao/cache/ImageCacheMapper.java | 24 ++ .../com/lion/lionwebsite/Domain/GidToKey.java | 14 ++ .../lionwebsite/Domain/ImageKeyCache.java | 10 + .../Service/GalleryManageService.java | 209 ++++++++++-------- .../lion/lionwebsite/Util/GalleryUtil.java | 86 ++++++- src/main/resources/cache.db | Bin 172032 -> 180224 bytes 8 files changed, 261 insertions(+), 111 deletions(-) create mode 100644 src/main/java/com/lion/lionwebsite/Dao/cache/ImageCacheMapper.java create mode 100644 src/main/java/com/lion/lionwebsite/Domain/GidToKey.java create mode 100644 src/main/java/com/lion/lionwebsite/Domain/ImageKeyCache.java diff --git a/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java b/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java index d00e1da..e814892 100644 --- a/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java +++ b/src/main/java/com/lion/lionwebsite/Configuration/CustomBean.java @@ -14,7 +14,7 @@ import org.springframework.context.annotation.Configuration; import com.pengrad.telegrambot.model.User; @Configuration -@RegisterReflectionForBinding(classes = {CustomConfiguration.class, +@RegisterReflectionForBinding(classes = {CustomConfiguration.class, GidToKey.class, ImageKeyCache.class, GalleryForQuery.class, Gallery.class, GalleryTask.class, HikariConfig.class, PageNameCache.class, ShareFile.class, Tag.class, TagMark.class, User.class, SendResponse.class, Message.class, com.pengrad.telegrambot.model.User.class, diff --git a/src/main/java/com/lion/lionwebsite/Controller/GalleryManageController.java b/src/main/java/com/lion/lionwebsite/Controller/GalleryManageController.java index 306bffe..8ae5b55 100644 --- a/src/main/java/com/lion/lionwebsite/Controller/GalleryManageController.java +++ b/src/main/java/com/lion/lionwebsite/Controller/GalleryManageController.java @@ -91,19 +91,30 @@ public class GalleryManageController { return galleryManageService.getWeekUsedAmount(); } - @GetMapping("/onlineImage/{page}") public void getOnlineImage(Integer gid, @PathVariable("page") Short page, HttpServletRequest request, HttpServletResponse response){ galleryManageService.getOnlineImage(gid, page, request, response); } - - @PostMapping("/share") - public String shareGallery(Integer gid, Integer userId, Integer expireHour){ - if(userId.equals(3)) - return galleryManageService.shareGallery(gid, expireHour); - return Response._failure("非法访问"); - } +// @PostMapping("/cache") +// public String cacheImageKeys(String url){ +// return galleryManageService.cacheImagesKey(url); +// } +// +// @GetMapping("/onlineImage/{page}") +// public void getCacheImage(String gid, @PathVariable("page") int page, HttpServletRequest request, HttpServletResponse response){ +// try { +// log.info("gid:" + gid + " page:" + page); +// galleryManageService.getCachedImage(gid, page, request, response); +// log.info("return gid:" + gid + " page:" + page); +// }catch (Exception e){ +// e.printStackTrace(); +// log.error(e.getMessage()); +// try { +// response.sendError(500); +// }catch (Exception ignore){} +// } +// } @PostMapping("/reset") public String resetUndone(){ diff --git a/src/main/java/com/lion/lionwebsite/Dao/cache/ImageCacheMapper.java b/src/main/java/com/lion/lionwebsite/Dao/cache/ImageCacheMapper.java new file mode 100644 index 0000000..df6255b --- /dev/null +++ b/src/main/java/com/lion/lionwebsite/Dao/cache/ImageCacheMapper.java @@ -0,0 +1,24 @@ +package com.lion.lionwebsite.Dao.cache; + +import com.lion.lionwebsite.Domain.GidToKey; +import com.lion.lionwebsite.Domain.ImageKeyCache; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface ImageCacheMapper { + @Insert("insert into ImageKeyCache values (#{gid}, #{imgkey}, #{page})") + void insertImageKeyCache(ImageKeyCache imageKeyCache); + + @Insert("insert into gidToKey values (#{gid}, #{key}, #{pages})") + void insertGidToKey(GidToKey gidToKey); + + @Select("select * from ImageKeyCache where gid=#{gid} and page=#{page}") + ImageKeyCache selectImageKeyCacheByGidAndPage(@Param("gid") String gid, @Param("page") int page); + + @Select("select * from gidToKey where gid=#{gid}") + GidToKey selectKeyByGid(String gid); + +} diff --git a/src/main/java/com/lion/lionwebsite/Domain/GidToKey.java b/src/main/java/com/lion/lionwebsite/Domain/GidToKey.java new file mode 100644 index 0000000..03c81b7 --- /dev/null +++ b/src/main/java/com/lion/lionwebsite/Domain/GidToKey.java @@ -0,0 +1,14 @@ +package com.lion.lionwebsite.Domain; + +import lombok.Data; + +@Data +public class GidToKey { + String gid; + String key; + int pages; + + public String toUrl(){ + return "https://exhentai.org/g/" + gid + "/" + key; + } +} diff --git a/src/main/java/com/lion/lionwebsite/Domain/ImageKeyCache.java b/src/main/java/com/lion/lionwebsite/Domain/ImageKeyCache.java new file mode 100644 index 0000000..aa20f06 --- /dev/null +++ b/src/main/java/com/lion/lionwebsite/Domain/ImageKeyCache.java @@ -0,0 +1,10 @@ +package com.lion.lionwebsite.Domain; + +import lombok.Data; + +@Data +public class ImageKeyCache { + String gid; + int page; + String imgkey; +} diff --git a/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java b/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java index b6aac9d..2157855 100644 --- a/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java +++ b/src/main/java/com/lion/lionwebsite/Service/GalleryManageService.java @@ -1,7 +1,7 @@ package com.lion.lionwebsite.Service; import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.RandomUtil; +import com.lion.lionwebsite.Dao.cache.ImageCacheMapper; import com.lion.lionwebsite.Dao.cache.PageNameCacheMapper; import com.lion.lionwebsite.Dao.normal.*; import com.lion.lionwebsite.Domain.*; @@ -16,23 +16,28 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.im4java.core.ConvertCmd; +import org.im4java.core.IM4JavaException; +import org.im4java.core.IMOperation; import org.springframework.stereotype.Service; import java.io.*; -import java.time.LocalDateTime; -import java.time.ZoneId; +import java.net.URI; +import java.net.URISyntaxException; import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; -import static com.lion.lionwebsite.Util.CustomUtil.dateTimeFormatter; import static com.lion.lionwebsite.Util.CustomUtil.fourZeroFour; +import static com.lion.lionwebsite.Util.CustomUtil.objectMapper; +import static com.lion.lionwebsite.Util.GalleryUtil.*; @Service @Data @Slf4j public class GalleryManageService { String TargetPath = "/root/gallery/"; - - int cacheSize = 100; + String cachePath = "/root/galleryCache/"; Map gid2name_cache = new HashMap<>(); @@ -54,12 +59,16 @@ public class GalleryManageService { PageNameCacheMapper pageNameCacheMapper; + ImageCacheMapper imageCacheMapper; + RemoteService remoteService; PushService pushService; + ExecutorService convertThread; + public GalleryManageService(GalleryMapper galleryMapper, CollectMapper collectMapper, CustomConfigurationMapper configurationMapper, UserMapper userMapper, ShareFileMapper shareFileMapper, - TagMapper tagMapper, PageNameCacheMapper pageNameCacheMapper, RemoteService remoteService, PushService pushService) { + TagMapper tagMapper, PageNameCacheMapper pageNameCacheMapper, RemoteService remoteService, PushService pushService, ImageCacheMapper imageCacheMapper) { this.galleryMapper = galleryMapper; this.collectMapper = collectMapper; this.configurationMapper = configurationMapper; @@ -69,6 +78,8 @@ public class GalleryManageService { this.pageNameCacheMapper = pageNameCacheMapper; this.remoteService = remoteService; this.pushService = pushService; + this.imageCacheMapper = imageCacheMapper; + convertThread = Executors.newFixedThreadPool(2); } /** @@ -123,12 +134,11 @@ public class GalleryManageService { } } } catch (ResolutionNotMatchException e) { - e.printStackTrace(); response.failure("提交失败,分辨率不存在"); pushService.taskCreateReport(user.getUsername(), link, response); return response.toJSONString(); } catch (IOException e) { - e.printStackTrace(); + log.error(e.getMessage()); response.failure("IO错误,可能是网络波动"); pushService.taskCreateReport(user.getUsername(), link, response); return response.toJSONString(); @@ -320,54 +330,6 @@ public class GalleryManageService { return response.toJSONString(); } -// /** -// * 获取本子图片的名字 -// * @param gid gid -// * @return 名字数组 -// */ -// public String selectOnlineFilename(Integer gid){ -// Response response = Response.generateResponse(); -// String path; -// -// if((path = gid2name_cache.get(gid)) == null) { -// Gallery gallery = galleryMapper.selectGalleryByGid(gid); -// -// if (gallery == null) { -// response.failure("本子不存在"); -// return response.toJSONString(); -// } -// -// path = TargetPath + gallery.getName(); -// gid2name_cache.put(gid, path); -// if(gid2name_cache.size() > cacheSize) -// gid2name_cache.remove(gid2name_cache.keySet().iterator().next()); -// } -// -// File file = new File(path); -// if(!file.isDirectory()){ -// response.failure("本子文件已被删除"); -// return response.toJSONString(); -// } -// -// File[] files = file.listFiles(pathname -> pathname.getName().endsWith(".webp") && !pathname.getName().startsWith("thumbnail")); -// if(files == null || files.length == 0){ -// response.failure("本子文件丢失"); -// return response.toJSONString(); -// } -// -// ArrayList images = new ArrayList<>(); -// for(File image: files){ -// images.add(image.getName().replace(".webp", "")); -// } -// //字符串长度相等时比较字典顺序,不相等时比较长度 -// images.sort((s1, s2) -> -// s1.length() == s2.length() ? s1.compareTo(s2): s1.length() - s2.length() -// ); -// -// response.success(new ObjectMapper().valueToTree(images).toString()); -// return response.toJSONString(); -// } - /** * 删除本子以及对应的文件(如果存在的话) * @@ -384,7 +346,7 @@ public class GalleryManageService { } ArrayList collector = collectMapper.selectCollectorByGid(gallery.getGid()); - if (!(collector.isEmpty() || collector.size() == 1 && collector.get(0).equals(user.getId()) //判断收藏 + if (!(collector.isEmpty() || collector.size() == 1 && collector.getFirst().equals(user.getId()) //判断收藏 && gallery.getDownloader() == user.getId())) //判断下载 response.failure("删除失败,该本子已被别人收藏或你不是下载人"); else { @@ -432,7 +394,7 @@ public class GalleryManageService { /** * 获取在线图片 * @param gid gid - * @param page 文件名 + * @param page 文件名 * @param request 请求对象 * @param response 响应对象 */ @@ -487,42 +449,105 @@ public class GalleryManageService { FileDownload.export(request, response, file.getAbsolutePath()); } - - - public String shareGallery(Integer gid, Integer expireHour){ + public String cacheImagesKey(String url) { Response response = Response.generateResponse(); - Gallery gallery = galleryMapper.selectGalleryByGid(gid); - Map jsonObject = new HashMap<>(); - if(gallery == null){ - response.failure("本子不存在"); - return response.toJSONString(); + String gid = String.valueOf(GalleryUtil.parseGid(url)); + GidToKey gidToKey = imageCacheMapper.selectKeyByGid(gid); + Gallery gallery; + //已缓存过,直接返回 + if(gidToKey != null) { + gallery = galleryMapper.selectGalleryByGid(Integer.parseInt(gid)); + return response.success(objectMapper.valueToTree(gallery)).toJSONString(); } - File file = new File(TargetPath + gallery.getName() + ".zip"); + try { + gidToKey = new GidToKey(); + gidToKey.setGid(gid); + gidToKey.setKey(url.split("/")[5].strip()); + gallery = GalleryUtil.parse(url, false, null); + ArrayList imageKeyCaches = GalleryUtil.parseImageKeys(url); + gidToKey.setPages(imageKeyCaches.size()); + imageCacheMapper.insertGidToKey(gidToKey); + for (ImageKeyCache imageKeyCache : imageKeyCaches) + imageCacheMapper.insertImageKeyCache(imageKeyCache); + response.success(objectMapper.valueToTree(gallery)); + }catch (IOException | ResolutionNotMatchException e){ + log.error(e.getMessage()); + response.failure("网络波动或其他异常"); + } + return response.toJSONString(); + } - if(!file.isFile()){ - response.failure("本子文件不存在"); - return response.toJSONString(); + String[] suffixes = {".webp", ".gif"}; + public void getCachedImage(String gid, Integer page, HttpServletRequest request, HttpServletResponse response) throws IOException, URISyntaxException { + //检查文件夹是否存在 + File folder = new File(cachePath + gid); + if(!folder.isDirectory()) + folder.mkdirs(); + + //检查对应图片是否存在,存在则直接返回 + for (String suffix : suffixes) { + if(new File(cachePath + gid + "/" + page + suffix).exists()){ + FileDownload.export(request, response, cachePath + gid + "/" + page + suffix); + return; + } } - ShareFile shareFile = shareFileMapper.selectShareFileByFilePath(file.getAbsolutePath()); - if(shareFile != null){ - jsonObject.put("shareCode", shareFile.getShareCode()); - jsonObject.put("expireTime", dateTimeFormatter.format(LocalDateTime.ofInstant(shareFile.getExpireTime().toInstant(), ZoneId.systemDefault()))); - response.success(new ObjectMapper().valueToTree(jsonObject).toString()); - return response.toJSONString(); + //检查该本子缓存是否存在 + GidToKey gidToKey = imageCacheMapper.selectKeyByGid(gid); + if(gidToKey == null) + try { + log.error("未缓存gid:" + gid); + response.sendError(404); + return; + }catch (IOException ignored){ + return; + } + + String imageUrl = null; + synchronized (this) { + //获取该图片key + ImageKeyCache imageKeyCache = imageCacheMapper.selectImageKeyCacheByGidAndPage(gid, page); + if (imageKeyCache == null) { + response.sendError(404); + return; + } + + //获取图片地址 + for (int i = 0; i < 2; i++) { + imageUrl = GalleryUtil.getImageUrl(getMpvKey(gidToKey.toUrl()), imageKeyCache); + if (imageUrl != null) + break; + GalleryUtil.refreshMpvKey(gidToKey.toUrl()); + } + if (imageUrl == null) + try { + log.error("获取图片url失败:gid=" + gid + " page=" + page + " imageKey=" + imageKeyCache.getImgkey()); + response.sendError(404); + return; + } catch (IOException ignored) { + return; + } } - - String ShareCode = RandomUtil.randomString(8); - Calendar expireTime = Calendar.getInstance(); - - expireTime.add(Calendar.HOUR, expireHour); - shareFileMapper.insertShareFile(ShareCode, file.getAbsolutePath(), expireTime.getTime()); - jsonObject.put("shareCode", ShareCode); - jsonObject.put("expireTime", dateTimeFormatter.format(expireTime.getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime())); - response.success(new ObjectMapper().valueToTree(jsonObject).toString()); - - return response.toJSONString(); + //下载图片,转格式并返回 + String suffix = imageUrl.substring(imageUrl.lastIndexOf(".")); + String imagePath = cachePath + gid + "/" + page + suffix; + new URI(imageUrl).toURL().openConnection().getInputStream().transferTo(new FileOutputStream(imagePath)); + FileDownload.export(request, response, imagePath); + if (!suffix.equals(".gif")) + convertThread.submit(() -> { + ConvertCmd convertCmd = new ConvertCmd(true); + IMOperation operation = new IMOperation(); + operation.addImage(imagePath); + operation.format("webp"); + operation.addImage(imagePath.replace("jpg", "webp").replace("png", "webp")); + try { + convertCmd.run(operation); + new File(imagePath).delete(); + }catch (IOException | IM4JavaException | InterruptedException e){ + log.error("文件" + imagePath + "转换失败"); + } + }); } public String resetUndone(){ @@ -545,12 +570,4 @@ public class GalleryManageService { return response.toJSONString(); } - - public static Integer parseGid(String link){ - try { - return Integer.parseInt(link.split("/g/")[1].split("/")[0]); - }catch (IndexOutOfBoundsException e){ - return null; - } - } } diff --git a/src/main/java/com/lion/lionwebsite/Util/GalleryUtil.java b/src/main/java/com/lion/lionwebsite/Util/GalleryUtil.java index 3a9f2d1..3d8a84d 100644 --- a/src/main/java/com/lion/lionwebsite/Util/GalleryUtil.java +++ b/src/main/java/com/lion/lionwebsite/Util/GalleryUtil.java @@ -1,6 +1,8 @@ package com.lion.lionwebsite.Util; +import com.fasterxml.jackson.databind.JsonNode; import com.lion.lionwebsite.Domain.Gallery; +import com.lion.lionwebsite.Domain.ImageKeyCache; import com.lion.lionwebsite.Exception.ResolutionNotMatchException; import org.apache.http.HttpEntity; import org.apache.http.client.entity.EntityBuilder; @@ -20,6 +22,9 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.lion.lionwebsite.Util.CustomUtil.objectMapper; public class GalleryUtil { @@ -30,11 +35,7 @@ public class GalleryUtil { static String JSON = "json"; - public static ArrayList galleriesForDownload; - - static { - galleriesForDownload = new ArrayList<>(); - } + static HashMap gid2MpvKey = new HashMap<>(); /** @@ -140,6 +141,71 @@ public class GalleryUtil { return gallery; } + public static ArrayList parseImageKeys(String url) throws IOException { + String temp = url.substring(url.indexOf("/g/") + 3); + String mpvUrl = "https://exhentai.org/mpv/" + temp; + String gid = url.split("/")[4]; + HashMap header = new HashMap<>(); + header.put("Referer", url); + String content = requests(mpvUrl, "get", header, null); + Document document = Jsoup.parse(content); + Element script = document.select("body > script").get(1); + String[] scripts = script.html().split("\n"); + ArrayList imageKeyCaches = new ArrayList<>(); + AtomicInteger page = new AtomicInteger(1); + gid2MpvKey.put(gid, scripts[1].split("=")[1].replace(";", "").replace("\"", "").replace(" ", "")); + JsonNode nodes = objectMapper.readValue(scripts[2].replace("var imagelist = ", ""), JsonNode.class); + nodes.forEach((n) -> { + ImageKeyCache imageKeyCache = new ImageKeyCache(); + imageKeyCache.setGid(gid); + imageKeyCache.setImgkey(n.get("k").asText()); + imageKeyCache.setPage(page.getAndIncrement()); + imageKeyCaches.add(imageKeyCache); + }); + return imageKeyCaches; + } + + public static String getMpvKey(String url) throws IOException { + String gid = String.valueOf(parseGid(url)); + if(!gid2MpvKey.containsKey(gid)) + refreshMpvKey(url); + return gid2MpvKey.get(gid); + } + + public static void refreshMpvKey(String url) throws IOException { + String temp = url.substring(url.indexOf("/g/") + 3); + String mpvUrl = "https://exhentai.org/mpv/" + temp; + HashMap header = new HashMap<>(); + header.put("Referer", url); + String content = requests(mpvUrl, "get", header, null); + Document document = Jsoup.parse(content); + Element script = document.select("body > script").get(1); + String[] scripts = script.html().split("\n"); + String mpvKey = scripts[1].split("=")[1].replace(";", "").replace("\"", "").replace(" ", ""); + gid2MpvKey.put(parseGid(url) + "", mpvKey); + System.out.println("刷新key:" + mpvKey); + } + + public static String getImageUrl(String mpvKey, ImageKeyCache imageKeyCache) throws IOException { + String apiUrl = "https://s.exhentai.org/api.php"; + HashMap header = new HashMap<>(); + header.put("Referer", "https://exhentai.org"); + HashMap body = new HashMap<>(); + body.put("gid", imageKeyCache.getGid()); + body.put("mpvkey", mpvKey); + body.put("imgkey", imageKeyCache.getImgkey()); + body.put("method", "imagedispatch"); + body.put("page", "" + imageKeyCache.getPage()); + body.put("payload", "json"); + System.out.println("开始获取imgurl:" + imageKeyCache.getGid() + ":" + imageKeyCache.getPage()); + String result = requests(apiUrl, "post", header, body); + System.out.println("获取imgurl成功:" + imageKeyCache.getGid() + ":" + imageKeyCache.getPage()); + JsonNode jsonNode = objectMapper.readTree(result); + if (jsonNode.has("error") && jsonNode.get("error").asText().equals("Key mismatch")) + return null; + return jsonNode.get("i").asText(); + } + /** * 验证链接 @@ -192,7 +258,7 @@ public class GalleryUtil { if((payload = body.remove("payload")).equals(JSON)) { EntityBuilder entityBuilder = EntityBuilder.create(); entityBuilder.setContentType(ContentType.APPLICATION_JSON); - entityBuilder.setText(CustomUtil.objectMapper.writeValueAsString(body)); + entityBuilder.setText(objectMapper.writeValueAsString(body)); httpPost.setEntity(entityBuilder.build()); } else if(payload.equals(FORM_DATA)) { MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create(); @@ -221,4 +287,12 @@ public class GalleryUtil { return stringBuilder.toString(); } + + public static Integer parseGid(String link){ + try { + return Integer.parseInt(link.split("/g/")[1].split("/")[0]); + }catch (IndexOutOfBoundsException e){ + return null; + } + } } diff --git a/src/main/resources/cache.db b/src/main/resources/cache.db index 6bb3ed70e7fa93d98a21c57be4f1dbcabd45040f..3a8e444f8121147cf7585dda160e940810182d75 100644 GIT binary patch delta 356 zcmZoTz}3*eJwaNKhk=1X2Z&*Sb)t^3Fb{)X-VR=%5Hnvc1AhVE55C-ug}-_08#$QS z#nsgr8~sZXlX6l$a}(23y;Ccl6O%JgIXccku8twD3L%b8KCTKV>bNwx6o4Q-GX+GI zB^D)TBo=8H8EEQ&#WQo$vr{WE#S4H|)dMwT=9Q$TrxtN(mNBu5%gQn~+Jl_}G%h3` z=oA>Ojp6{9D2m+}R)g%t;v$gE#bA3`!4@|c@V6K6Gj1>7XPPj94HQZY0vike^G$5{ HjmiT6dM9JP delta 64 zcmZo@;BGj;H9=aCi-Cbb3y5KWd7_RnKNo{u-VR=%5F>v91AoDG!39iA{LKaY?FIad L+Y9)aCQJYTWYr9g