Ejemplo n.º 1
0
 async def nhentai(self,
                   ctx: commands.Context,
                   djid=None,
                   *,
                   extra: str = ''):
     '''finds doujins on nhentai by id'''
     if ctx.invoked_subcommand is not None:
         return
     if djid is None:
         await ctx.send_help(ctx.command)
         return
     if not Hentai.exists(djid):
         return await self.search(ctx, tags=f'{djid} {extra}')
     result = Hentai(djid)
     tags = [_.name for _ in result.tag]
     e = discord.Embed(title=result.title(Format.Pretty),
                       description=f'#{result.id}',
                       url=result.url,
                       color=int(djid))
     e.set_image(url=result.cover)
     e.set_footer(text=result.upload_date)
     e.add_field(name="Tags", value=', '.join(tags))
     artists = ', '.join([_.name for _ in result.artist
                          ]) if result.artist != [] else 'None'
     e.add_field(name="Artist(s)", value=artists)
     await ctx.send(embed=e)
Ejemplo n.º 2
0
    def make_embed(hnt: Hentai):
        def convert(tag: list) -> str:
            lst = list(map(lambda x: x.name, tag))
            return ", ".join(lst)
        
        def convert_list(tag: list) -> list:
            return list(map(lambda x: x.name, tag))

        embed = discord.Embed(
            colour = discord.Colour.random(),
            title = hnt.title(Format.Pretty),
            url = f"https://nhentai.net/g/{hnt.id}"
        )

        embed.set_thumbnail(
            url=hnt.thumbnail
        )
        embed.set_image(url=hnt.cover)

        description = \
            f"`id`      :    {hnt.id}\n" + \
            f"`pages`   :    {hnt.num_pages}\n" + \
            f"`tags`    :    {convert(hnt.tag)}\n"

        if hnt.artist: description += f"`artist`   :   {convert(hnt.artist)}"

        embed.description = description
        embed.set_footer(text="Speedwagon Foundation got ur back")           
        
        for e in convert_list(hnt.tag):
            if e == "lolicon":
                embed.set_footer(text="Speedwagon Foundation can't cover u from FBI")
            
        embed.timestamp = datetime.datetime.utcnow()
        return embed
Ejemplo n.º 3
0
    async def read(self, ctx, code):
        async with ctx.typing():
            try:
                doujin = Hentai(code)
            except HTTPError:
                return UI.error_embed(f'Doujin dengan code {code} tidak ada')

            pages = [image for image in doujin.image_urls]
            curr_index = 1
            max_index = len(pages)
            title = doujin.title(Format.Pretty)
            embed, components = self.helpers.generate_read_hentai_embed(title, pages[curr_index-1], curr_index, max_index)

            nhentai = await ctx.send(embed=embed, components=components)
        
        try:
            while True:
                btn = await nhentai.wait_for('button', self.bot, timeout=60)
                await btn.respond(ninja_mode=True)

                if btn.custom_id == 'hentai_first': curr_index = 1
                if btn.custom_id == 'hentai_prev': curr_index -= 1
                if btn.custom_id == 'hentai_next': curr_index += 1
                if btn.custom_id == 'hentai_last': curr_index = max_index

                embed, components = self.helpers.generate_read_hentai_embed(title, pages[curr_index-1], curr_index, max_index)
                await nhentai.edit(embed=embed, components=components)
        except TimeoutError:
            embed, components = self.helpers.generate_read_hentai_embed(title, pages[curr_index-1], curr_index, max_index, disable_components=True)
            await nhentai.edit(components=components)
        except NotFound:
            pass
Ejemplo n.º 4
0
    def make_page_embed(ctx: Context, hnt: Hentai, page: int):

        """
        This function converts a `getpage` query to a suitable Embed.
        :raises: ValueError if the page number provided is invalid
        :param ctx: context
        :param hnt: the hentai
        :param page: the desired page number
        :return: an Embed or Raises an Error
        """

        if page < 1 or page > hnt.num_pages:
            raise ValueError(f"[Invalid page **{page}**")

        embed = discord.Embed(
            title=hnt.title(Format.Pretty) + f" || page {page}",
            url=NHentaiCommands.hentai_url(hnt)+f"{page}",
            colour=NHentaiCommands.get_random_color()
        )
        embed.set_image(url=hnt.pages[page - 1].url)
        embed.set_footer(
            text=f"requested by || {ctx.author.name}({ctx.author.display_name})",
            icon_url=ctx.author.avatar_url
        )

        return embed
Ejemplo n.º 5
0
    def make_embed(hnt: Hentai) -> discord.Embed:
        """
        This class method takes a NHentai manga element and turns it into a 
        discord Embed and returns it

        """

        def convert(tag: list) -> str:
            lst = list(map(lambda x: x.name, tag))
            return ", ".join(lst)

        embed = discord.Embed(
            colour=NHentaiCommands.get_random_color(),
            title=hnt.title(Format.Pretty),
            url=f"https://nhentai.net/g/{hnt.id}"
        )

        embed.set_thumbnail(
            url=hnt.thumbnail
        )
        embed.set_image(url=hnt.cover)

        description = \
            f"`id`      :    {hnt.id}\n" + \
            f"`pages`   :    {hnt.num_pages}\n" + \
            f"`tags`    :    {convert(hnt.tag)}\n"

        if hnt.artist: description += f"artist   :   {convert(hnt.artist)}"

        embed.description = description
        return embed
Ejemplo n.º 6
0
 async def rnd(self, ctx: commands.Context):
     """Random one"""
     doujin = Hentai(Utils.get_random_id())
     embed_list = []
     for i in doujin.image_urls:
         embed = Embed.default(ctx)
         embed.title = doujin.title(Format.Pretty)
         embed.set_image(url=i)
         embed_list.append(embed)
     await menus.MenuPages(
         source=EmbedListMenu(embed_list),
         clear_reactions_after=True,
     ).start(ctx=ctx, wait=False)
Ejemplo n.º 7
0
def nuke_codes():
    if request.method == 'POST':
        data = request.get_json()
        code = data['code']
        print(data)
        if (Hentai.exists(code)):
            doujin = Hentai(code)

            print(f"\n#{code} : success")
            print(f"title : {doujin.title()}")
            print(f"tags : {[tag.name for tag in doujin.tag]}")
            print(f"poster url : {doujin.image_urls[0]}\n")

            return {
                'id':
                doujin.id,
                'title_release':
                doujin.title(),
                'title_pretty':
                doujin.title(Format.Pretty),
                'tags': [tag.name for tag in doujin.tag],
                'poster_link':
                doujin.image_urls[0],
                # 'poster_blob' : poster_blob,
                'artist': [{
                    'name': artist.name,
                    'url': artist.url,
                    'count': artist.count
                } for artist in doujin.artist],
                'languages': [lang.name for lang in doujin.language],
                'categories': [cat.name for cat in doujin.category],
                'pages':
                doujin.num_pages,
                'uploaded':
                doujin.upload_date
            }
        else:
            return None
    return None
Ejemplo n.º 8
0
    async def sauce(self, ctx, nuke_code):
        """ What's behind that nuke code? """
        message = await ctx.send("Extracting sauce from nuke code... 👨‍💻")

        if Hentai.exists(nuke_code):
            doujin = Hentai(nuke_code)
            english = doujin.title()
            japanese = doujin.title(Format.Japanese)
            pages = str(doujin.num_pages)
            hentai_id = str(doujin.id)
            link = "||" + doujin.url + "||"
            tag = (getattr(i, 'name') for i in doujin.tag)
            content_list = (
                f'**Hentai ID**: {hentai_id}',
                f'**Thumbnail**: {doujin.thumbnail}',
                f'** English Title**: {english if english else "none"}',
                f'**Japanese Title**: {japanese if japanese else "none"}',
                f'**Pages**: {pages}',
                f'**Tags**: {", ".join(tag)}',
                f'**Link**: {link}'
            )
            await message.edit(content="\n".join(content_list))
        else:
            await message.edit(content="The sauce you are looking for does not exist. 🤷‍♂️")
Ejemplo n.º 9
0
 async def read(self, ctx: commands.Context, digits):
     """Read doujins."""
     if not digits.isdigit():
         return await ctx.send("Only digits allowed.")
     if not Hentai.exists(digits):
         return await ctx.send("Doesn't exist.")
     doujin = Hentai(digits)
     embed_list = []
     for i in doujin.image_urls:
         embed = Embed.default(ctx)
         embed.title = doujin.title(Format.Pretty)
         embed.set_image(url=i)
         embed_list.append(embed)
     await menus.MenuPages(
         source=EmbedListMenu(embed_list),
         clear_reactions_after=True,
     ).start(ctx=ctx, wait=False)
Ejemplo n.º 10
0
 async def lookup(self, ctx: commands.Context, doujin):
     """ Info about a doujin."""
     if not doujin.isdigit():
         return await ctx.send("Only digits allowed.")
     if not Hentai.exists(doujin):
         return await ctx.send("Doesn't exist.")
     doujin = Hentai(doujin)
     embed = Embed.default(ctx)
     embed.title = doujin.title(Format.Pretty)
     embed.add_field(name="Holy Digits", value=doujin.id, inline=True)
     embed.add_field(name="Languages",
                     value=Tag.get(doujin.language, "name"),
                     inline=True)
     embed.add_field(name="Uploaded", value=doujin.upload_date, inline=True)
     embed.add_field(name="Number of times liked",
                     value=doujin.num_favorites,
                     inline=True)
     embed.add_field(name="Tags", value=Tag.get(doujin.tag, "name"))
     embed.add_field(name="Number of pages", value=doujin.num_pages)
     embed.set_thumbnail(url=doujin.thumbnail)
     await ctx.send(embed=embed)
Ejemplo n.º 11
0
 async def lookup_id(self, ctx, id: int):
     if not Hentai.exists(id):
         await ctx.send("Error: Invalid ID.")
     else:
         doujin = Hentai(id)
         embed = discord.Embed(
             title=doujin.title(Format.Pretty),
             description=f"🌍 {', '.join(Tag.get_names(doujin.language))}",
             url=doujin.url,
             color=discord.Color.red())
         embed.add_field(name="Author", value=Tag.get_names(doujin.artist))
         embed.add_field(name="Favorites",
                         value=f"❤ {doujin.num_favorites}")
         embed.add_field(name="Pages", value=f"📕 {doujin.num_pages}")
         embed.set_thumbnail(url=doujin.thumbnail)
         embed.set_footer(
             text=f"Tags: {', '.join(Tag.get_names(doujin.tag))}")
         await self.client.change_presence(
             status=discord.Status.online,
             activity=discord.Game(
                 f"Now reading {doujin.title(Format.Pretty)}🥰"))
         await ctx.send(embed=embed)
Ejemplo n.º 12
0
 async def nhentai(self, ctx: commands.Context, djid: int = None):
     '''finds doujins on nhentai by id'''
     if ctx.invoked_subcommand is not None:
         return
     if djid is None:
         await ctx.send_help(ctx.command)
         return
     if not Hentai.exists(djid):
         await ctx.send(content="Doujin does not exist!")
         return
     result = Hentai(djid)
     tags = [_.name for _ in result.tag]
     e = discord.Embed(title=result.title(Format.Pretty),
                       description=f'#{result.id}',
                       url=result.url,
                       color=0x177013)
     e.set_image(url=result.cover)
     e.set_footer(text=result.upload_date)
     e.add_field(name="Tags", value=', '.join(tags))
     e.add_field(name="Artist(s)",
                 value=', '.join([_.name for _ in result.artist]))
     await ctx.send(embed=e)
Ejemplo n.º 13
0
    async def read_id(self, ctx, id: int):
        if not Hentai.exists(id):
            await ctx.send("Error: Invalid ID.")
        else:
            doujin = Hentai(id)
            reactions = {
                'prev': Emoji[':arrow_left:'],
                'next': Emoji[':arrow_right:']
            }

            embed = discord.Embed(title=doujin.title(Format.Pretty),
                                  description=f"Page 1 of {doujin.num_pages}",
                                  color=discord.Color.red())
            embed.set_image(url=doujin.cover)

            # TODO: implement emoji reaction event handler for pagination
            message = await ctx.send(embed=embed)
            self.reader_id = message.id
            print(type(message))
            print(self.reader_id)

            for emoji in reactions.values():
                await message.add_reaction(emoji)
Ejemplo n.º 14
0
async def _(event):
    if event.fwd_from:
        return
    input_str = event.pattern_match.group(1)
    code = input_str
    if "nhentai" in input_str:
        link_regex = r"(https?://)?(www\.)?nhentai\.net/g/(\d+)"
        match = re.match(link_regex, input_str)
        code = match.group(3)
    await event.edit("☙ `Searching for doujin...` ❧")
    try:
        doujin = Hentai(code)
    except BaseException as n_e:
        if "404" in str(n_e):
            return await event.edit(f"`{code}` is not found!")
        return await event.edit(f"**Error: **`{n_e}`")
    msg = ""
    imgs = ""
    for url in doujin.image_urls:
        imgs += f"<img src='{url}'/>"
    imgs = f"&#8205; {imgs}"
    title = doujin.title()
    graph_link = post_to_telegraph(title, imgs)
    msg += f"[{title}]({graph_link})"
    msg += f"\n**Source :**\n[{code}]({doujin.url})"
    if doujin.parody:
        msg += "\n**Parodies :**"
        parodies = []
        for parody in doujin.parody:
            parodies.append("#" + parody.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(parodies))
    if doujin.character:
        msg += "\n**Characters :**"
        charas = []
        for chara in doujin.character:
            charas.append("#" + chara.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(charas))
    if doujin.tag:
        msg += "\n**Tags :**"
        tags = []
        for tag in doujin.tag:
            tags.append("#" + tag.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(tags))
    if doujin.artist:
        msg += "\n**Artists :**"
        artists = []
        for artist in doujin.artist:
            artists.append("#" + artist.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(artists))
    if doujin.language:
        msg += "\n**Languages :**"
        languages = []
        for language in doujin.language:
            languages.append("#" + language.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(languages))
    if doujin.category:
        msg += "\n**Categories :**"
        categories = []
        for category in doujin.category:
            categories.append("#" + category.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(categories))
    msg += f"\n**Pages :**\n{doujin.num_pages}"
    await event.edit(msg, link_preview=True)
Ejemplo n.º 15
0
class CatalogueEntry:
    markdown_map = {
        "WhatsApp": {
            "bold": ["*", "*"],
            "italic": ["_", "_"],
            "scode": ["```", "```"],
            "mcode": ["```", "```"],
        },
        "Telegram": {
            "bold": ["**", "**"],
            "italic": ["__", "__"],
            "scode": ["`", "`"],
            "mcode": ["```\n", "\n```"],
        },
    }

    def __init__(
        self,
        eid: int,
        hid: int,
        desc: str = "",
        markdown: str = "Telegram",
    ) -> None:
        self.config = data.Config()

        self.submission = "Official Submission"
        self.rating = ""

        if not Path(self.config["Repository"]["path"]).is_dir():
            print(
                f'categen: {self.config["Repository"]["path"]} is non-existant, '
                "attempting to download Format.")
            try:
                data.Format.retrieve_online(
                    url=self.config["Repository"]["link"],
                    path=self.config["Repository"]["path"],
                )

            except Exception as e:
                print(
                    "categen: Error encountered while attempting to download format. "
                    f"({e}) categen will use the local Format shipped with the package."
                )
                data.Format.retrieve_local(
                    format_dir=self.config["Repository"]["path"])

        self.format = data.Format(repo_config=self.config["Repository"])
        self.doujin = Hentai(hid)

        self.markdown = self.markdown_map[markdown]
        self.id = "0" * (3 - len(str(eid))) + str(eid)
        self.description = str(desc)

        assert self.config["Settings"]["divide"] in [
            "both", "left", "right"
        ], (f'config:General:divide - "{self.config["Settings"]["divide"]}"'
            "is not a valid divide value!")
        self.divide = self.config["Settings"]["divide"]

        placements_path = Path(
            self.config["Repository"]["path"]).joinpath("placements.json")
        assert placements_path.exists(), f"{placements_path}: Non-existant"
        with open(placements_path, "r", encoding="utf-8") as pf:
            self.placements = load(pf)

        entry_path = Path(
            self.config["Repository"]["path"]).joinpath("entry.txt")
        assert entry_path.exists(), f"{entry_path}: Non-existant"
        with open(entry_path, "r", encoding="utf-8") as ef:
            self.entry_text = ef.read()

        tagsofinterest_path = Path(
            self.config["Repository"]["path"]).joinpath("tagsofinterest.json")
        assert tagsofinterest_path.exists(
        ), f"{tagsofinterest_path}: Non-existant"
        with open(tagsofinterest_path, "r", encoding="utf-8") as toif:
            self.tagsofinterest = load(toif)

        formatting_map_path = Path(
            self.config["Repository"]["path"]).joinpath("formatting.json")
        if formatting_map_path.is_file():
            with open(formatting_map_path, "r", encoding="utf-8") as fmf:
                self.formatting_map = load(fmf)
        else:
            self.formatting_map = {}

        template_path = Path(self.config["Repository"]["path"]).joinpath(
            "thumbnail/template.png")
        assert template_path.is_file(), f"{template_path}: Non-existant"
        self.template = Image.open(str(template_path))

        self._thumbnail = None
        self._entry = None

    def _divide(self, text: str) -> str:
        if "|" in text and self.divide != "both":
            if self.divide == "left":
                divided = text.split("|")[0].lstrip().rstrip()
            else:
                divided = text.split("|")[1].lstrip().rstrip()

            return divided

        else:
            return text

    def _strf(self,
              text: str,
              markdown_type: str = None,
              disable_override: bool = False) -> str:
        def fword(keyword: str) -> str:
            override = self.formatting_map.get(keyword, None)
            return (f"{keyword}:-sep-:{override}" if not any(
                [override is None, override == "", disable_override]) else
                    keyword)

        if markdown_type is None:
            markdown_type = self.config["Settings"]["markdown_type"]

        # Title Generation
        title_p_en = self._divide(self.doujin.title(Format.Pretty))
        title_p_jp = self._divide(
            Utils.sanitize(self.doujin.title(Format.Japanese)))

        markdown = self.markdown_map[markdown_type]

        bold = markdown["bold"][0]
        _bold = markdown["bold"][1]

        if title_p_en != "" and title_p_jp != "":
            title = f"{bold}{title_p_en}{_bold} - {bold}{title_p_jp}{_bold}"
        elif title_p_en != "" and title_p_jp == "":
            title = f"{bold}{title_p_en}{_bold}"
        elif title_p_en == "" and title_p_jp != "":
            title = f"{bold}{title_p_jp}{_bold}"
        else:
            title = "Unnamed"

        # Creator Text Generation
        artist_names = [tag.name for tag in self.doujin.artist]
        artist_names_str = ", ".join(artist_names)

        group_names = [tag.name for tag in self.doujin.group]
        group_names_str = ", ".join(group_names)

        if len(artist_names) > 0 and len(group_names) > 0:
            creator = f"({artist_names_str} / {group_names_str})"
        elif len(artist_names) > 0 and len(group_names) == 0:
            creator = f"({artist_names_str})"
        elif len(artist_names) == 0 and len(group_names) > 0:
            creator = f"({group_names_str})"
        else:
            creator = "(No Creator Info)"

        # Tags
        tags = [self._divide(tag.name) for tag in self.doujin.tag]
        tags_interest = [tag for tag in tags if tag in self.tagsofinterest]
        tags_remainder = [tag for tag in tags if tag not in tags_interest]

        format_map = {
            "bold":
            markdown["bold"][0],
            "-bold":
            markdown["bold"][1],
            "italic":
            markdown["italic"][0],
            "-italic":
            markdown["italic"][1],
            "scode":
            markdown["scode"][0],
            "-scode":
            markdown["scode"][1],
            "mcode":
            markdown["mcode"][0],
            "-mcode":
            markdown["mcode"][1],
            fword("entry_id"):
            self.id,
            fword("description"):
            self.description,
            fword("title.english"):
            self._divide(self.doujin.title()),
            fword("title.english.pretty"):
            title_p_en,
            fword("title.japanese:"):
            self._divide(self.doujin.title(Format.Japanese)),
            fword("title.japanese.pretty"):
            title_p_jp,
            fword("title"):
            title,
            fword("tags.interest"):
            ", ".join(tags_interest),
            fword("tags.remainder"):
            ", ".join(tags_remainder),
            fword("tags"):
            ", ".join(tags),
            fword("creator.scanlator"):
            self.doujin.scanlator,
            fword("creator.artist"):
            ", ".join([t.name for t in self.doujin.artist]),
            fword("creator.group"):
            ", ".join([t.name for t in self.doujin.group]),
            fword("creator"):
            creator,
            fword("doujin_id"):
            self.doujin.id,
            fword("pages"):
            self.doujin.num_pages,
            fword("favourites"):
            self.doujin.num_favorites,
            fword("link"):
            self.doujin.url,
            fword("time"):
            datetime.fromtimestamp(
                self.doujin.epos).strftime("%B %d %Y, %H:%M:%S"),
            fword("parody"):
            ", ".join([t.name for t in self.doujin.parody]),
            fword("characters"):
            ", ".join([t.name for t in self.doujin.character]),
            fword("slink"):
            f"nh.{self.doujin.id}",
            fword("submission"):
            self.submission,
            fword("rating"):
            self.rating,
        }

        for keyword, replacement in format_map.items():
            conditions = [
                "parody" in keyword and replacement == "original",
                "parody" in keyword and replacement == "",
                "favourites" in keyword and replacement == "0",
                "characters" in keyword and replacement == "",
                "creator.group" in keyword and replacement == "",
                "description" in keyword and replacement == "",
                "tags.remainder" in keyword and replacement == "",
                "rating" in keyword and replacement == "",
            ]
            if not any(conditions):
                if ":-sep-:" in keyword:
                    # keyword:-sep-:\nkeyword\n
                    _keyword = keyword
                    keyword = keyword.split(":-sep-:")[0]
                    override = _keyword.lstrip(keyword + ":-sep-:")

                    replacement = self._strf(
                        text=override,
                        markdown_type=markdown_type,
                        disable_override=True,
                    )

                text = text.replace(f"<[{keyword}]>", str(replacement))

            else:
                if ":-sep-:" in keyword:
                    keyword = keyword.split(":-sep-:")[0]

                text = text.replace(f"<[{keyword}]>", "")

        return text

    def thumbnail(self,
                  preview: Image.Image,
                  suppress: bool = False) -> Image.Image:
        placements = deepcopy(self.placements)

        if ".meta" in placements:
            placements.pop(".meta")

        for area in [
                area for area in placements
                if placements[area].get("type") == "text"
        ]:
            placements[area]["text"] = self._strf(placements[area]["text"])

        for area in [
                area for area in placements if placements[area]["path"] != ""
        ]:
            placements[area]["path"] = str(
                Path(self.config["Repository"]["path"]).joinpath(
                    placements[area]["path"]))

        placements["viewfinder"]["path"] = preview

        self._thumbnail = operate(
            image=self.template,
            placements=Placements.parse(placements),
            suppress=suppress,
        )
        return self._thumbnail

    def entry(self, markdown_type: str = None) -> str:
        self._entry = self._strf(self.entry_text, markdown_type=markdown_type)
        return self._entry

    def export(self, level: int = 4) -> bytes:
        from bz2 import compress
        from dill import dumps

        return compress(dumps(self), compresslevel=level)

    def pure_export(self) -> bytes:
        from dill import dumps

        return dumps(self)
Ejemplo n.º 16
0
async def _(event):
    if event.fwd_from:
        return
    await event.edit("`Mencari doujin...`")
    input_str = event.pattern_match.group(1)
    code = input_str
    if "nhentai" in input_str:
        link_regex = r"(?:https?://)?(?:www\.)?nhentai\.net/g/(\d+)"
        match = re.match(link_regex, input_str)
        code = match.group(1)
    if input_str == "random":
        code = Utils.get_random_id()
    try:
        doujin = Hentai(code)
    except BaseException as n_e:
        if "404" in str(n_e):
            return await event.edit(f"Doujin tidak ditemukan untuk `{code}`")
        return await event.edit(f"**Kesalahan :** `{n_e}`")
    msg = ""
    imgs = ""
    for url in doujin.image_urls:
        imgs += f"<img src='{url}'/>"
    imgs = f"&#8205; {imgs}"
    title = doujin.title()
    graph_link = post_to_telegraph(title, imgs)
    msg += f"[{title}]({graph_link})"
    msg += f"\n**Sumber :**\n[{code}]({doujin.url})"
    if doujin.parody:
        msg += "\n**Parodi :**"
        parodies = []
        for parody in doujin.parody:
            parodies.append("#" +
                            parody.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(parodies))
    if doujin.character:
        msg += "\n**Karakter :**"
        charas = []
        for chara in doujin.character:
            charas.append("#" + chara.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(charas))
    if doujin.tag:
        msg += "\n**Tag :**"
        tags = []
        for tag in doujin.tag:
            tags.append("#" + tag.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(tags))
    if doujin.artist:
        msg += "\n**Artis :**"
        artists = []
        for artist in doujin.artist:
            artists.append("#" +
                           artist.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(artists))
    if doujin.language:
        msg += "\n**Bahasa :**"
        languages = []
        for language in doujin.language:
            languages.append("#" +
                             language.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(languages))
    if doujin.category:
        msg += "\n**Kategori :**"
        categories = []
        for category in doujin.category:
            categories.append(
                "#" + category.name.replace(" ", "_").replace("-", "_"))
        msg += "\n" + " ".join(natsorted(categories))
    msg += f"\n**Halaman :**\n{doujin.num_pages}"
    await event.edit(msg, link_preview=True)