Repository: AlexandreSenpai/Enma (language: python)

Issue:
add import



Discussion / hints:

Fix patch:
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..4d3df00
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,9 @@
+coverage:
+  status:
+    patch:
+      default:
+        target: 90%
+        threshold: 5%
+  ignore:
+    - "setup.py"
+    - "tests/test_*.py"
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
new file mode 100644
index 0000000..1830e12
--- /dev/null
+++ b/.github/workflows/codecov.yml
@@ -0,0 +1,30 @@
+name: Code Coverage
+on:
+  push:
+    branches:
+      - "master"
+      - "dev"
+jobs:
+  codecov:
+    name: Codecov
+    runs-on: windows-latest
+
+    steps:
+      - uses: actions/checkout@v1
+      - name: Set up Python
+        uses: actions/setup-python@master
+        with:
+          python-version: 3.9
+      - name: Install Dependencies
+        run: |
+          pip install -r requirements.txt
+      - name: Generate Code Coverage Report
+        run: |
+          pip install pytest pytest-cov
+          pytest --cov=. --cov-report=xml ./tests
+      - name: Upload Report to codecov website
+        uses: codecov/codecov-action@v1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          file: ./coverage.xml
+          flags: pytest
diff --git a/.github/workflows/dev-pypi.yml b/.github/workflows/dev-pypi.yml
new file mode 100644
index 0000000..822d239
--- /dev/null
+++ b/.github/workflows/dev-pypi.yml
@@ -0,0 +1,34 @@
+name: Development Pypi
+on:
+  push:
+    branches:
+      - "dev"
+jobs:
+  pypi:
+    name: Pypi
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v1
+      - name: Set up Python
+        uses: actions/setup-python@master
+        with:
+          python-version: 3.9
+      - name: Setting environment name
+        run: |
+          environment_name='dev'
+      - name: Changing package name
+        run: |
+          mv NHentai ./dev_nhentai
+      - name: Create dist folder
+        run: |
+          python setup.py sdist --dist-dir=./dev_dist
+      - name: Checking folder
+        run: |
+          ls
+      - name: Publish a Python distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          packages_dir: dev_dist/
diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml
new file mode 100644
index 0000000..de09763
--- /dev/null
+++ b/.github/workflows/pypi.yml
@@ -0,0 +1,31 @@
+name: Development Pypi
+on:
+  push:
+    branches:
+      - "master"
+jobs:
+  pypi:
+    name: Pypi
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v1
+      - name: Set up Python
+        uses: actions/setup-python@master
+        with:
+          python-version: 3.9
+      - name: Setting environment name
+        run: |
+          environment_name='prd'
+      - name: Create dist folder
+        run: |
+          python setup.py sdist --dist-dir=./dist
+      - name: Checking folder
+        run: |
+          ls
+      - name: Publish a Python distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          packages_dir: dist/
diff --git a/NHentai/__init__.py b/NHentai/__init__.py
index 97ce6c7..2c07c04 100644
--- a/NHentai/__init__.py
+++ b/NHentai/__init__.py
@@ -1,5 +1,20 @@
+import sys
+
 from . import entities
 from . import nhentai
 from . import nhentai_async
 
-__all__ = ['nhentai', 'nhentai_async', 'entities']
\ No newline at end of file
+__all__ = ['nhentai', 'nhentai_async', 'entities']
+__version__ = '0.0.16'
+
+package_name = "nhentai-api"
+python_major = "3"
+python_minor = "7"
+
+try:
+    assert sys.version_info >= (int(python_major), int(python_minor))
+except AssertionError:
+    raise RuntimeError(f"\033[31m{package_name!r} requires Python {python_major}.{python_minor}+ (You have Python {sys.version})\033[0m")
+
+NHentai = nhentai.NHentai
+NHentaiAsync = nhentai_async.NHentaiAsync
diff --git a/NHentai/base_wrapper.py b/NHentai/base_wrapper.py
index adebcc1..6ad441f 100644
--- a/NHentai/base_wrapper.py
+++ b/NHentai/base_wrapper.py
@@ -1,57 +1,46 @@
 from asyncio import get_event_loop
 from asyncio import AbstractEventLoop
+from urllib.parse import urljoin
+from typing import Union
 
 from bs4 import BeautifulSoup
 from aiohttp import ClientSession
 import requests
 
-from .entities.doujin import Doujin, DoujinThumbnail
-from .entities.page import HomePage, SearchPage, TagListPage, GroupListPage, CharacterListPage, ArtistListPage, PopularPage
-from .entities.links import CharacterLink 
-
 class BaseWrapper:
-    def __init__(self):
-        self._BASE_URL = 'https://nhentai.net'
-        self._IMAGE_BASE_URL = 'https://i.nhentai.net/galleries'
-        self._SUPORTED_LANG = {'English': 'english', 'Chinese': 'chinese'}
-        self._event_loop = get_event_loop()
+
+    _BASE_URL = 'https://nhentai.net/'
+    _API_URL = 'https://nhentai.net/api/'
+    _IMAGE_BASE_URL = 'https://i.nhentai.net/galleries/'
+    _TINY_IMAGE_BASE_URL = _IMAGE_BASE_URL.replace('/i.', '/t.')
+
+    _event_loop = get_event_loop()
+
+    def __init__(self, cache_size: int=100):
+        self.cache_size: cache_size
     
     @property
     def event_loop(self) -> AbstractEventLoop:
         return self._event_loop
 
-    def _get_lang_by_title(self, title: str) -> str:
-        """This method runs through the title inputed and search by
-        one of supported languages if it doesnt finds the methods returns
-        Japanese.
-        """
-
-        acceptable_title = title.replace('[', '').replace(']', '')
-        partitoned_title = acceptable_title.split(' ')
-
-        lang = 'japanese'
-
-        for part in partitoned_title:
-            current_language_key = self._SUPORTED_LANG.get(part)
-            lang = current_language_key if current_language_key is not None else lang 
-        
-        return lang
-
-    def _fetch(self, page_path: str) -> BeautifulSoup:
+    def _fetch(self, page_path: str, params={}, is_json: bool=False) -> Union[BeautifulSoup, dict]:
         page_path = page_path[1:] if page_path[0] == '/' else page_path
-        PAGE_REQUEST = requests.get(f'{self._BASE_URL}/{page_path}')
+        PAGE_REQUEST = requests.get(urljoin(self._BASE_URL, page_path), params=params)
         if PAGE_REQUEST.status_code == 200:
-            return BeautifulSoup(PAGE_REQUEST.content, 'html.parser')
+            return PAGE_REQUEST.json() if is_json else BeautifulSoup(PAGE_REQUEST.content, 'html.parser')
         else:
-            return BeautifulSoup("", 'html.parser')
+            return PAGE_REQUEST.json() if is_json else BeautifulSoup("", 'html.parser')
     
-    async def _async_fetch(self, page_path: str) -> BeautifulSoup:
+    async def _async_fetch(self, page_path: str, params={}, is_json: bool=False) -> Union[BeautifulSoup, dict]:
         page_path = page_path[1:] if page_path[0] == '/' else page_path
         async with ClientSession() as session:
-            async with session.get(f'{self._BASE_URL}/{page_path}') as response:
+            async with session.get(urljoin(self._BASE_URL, page_path), params=params) as response:
                 if response.status == 200:
-                    CONTENT = await response.read()
-                    return BeautifulSoup(CONTENT, 'html.parser')
+                    if is_json:
+                        CONTENT = await response.json()
+                        return CONTENT
+                    else:
+                        CONTENT = await response.read()
+                        return BeautifulSoup(CONTENT, 'html.parser')
                 else:
-                    return BeautifulSoup("", 'html.parser')
-    
\ No newline at end of file
+                    return await response.json() if is_json else BeautifulSoup("", 'html.parser')
diff --git a/NHentai/entities/__init__.py b/NHentai/entities/__init__.py
index 56ad154..3c69d67 100644
--- a/NHentai/entities/__init__.py
+++ b/NHentai/entities/__init__.py
@@ -1,3 +1,4 @@
 from . import doujin
 from . import page
-from . import links
\ No newline at end of file
+from . import links
+from . import options
\ No newline at end of file
diff --git a/NHentai/entities/base_entity.py b/NHentai/entities/base_entity.py
new file mode 100644
index 0000000..9d02fb1
--- /dev/null
+++ b/NHentai/entities/base_entity.py
@@ -0,0 +1,5 @@
+from dataclasses import asdict
+
+class BaseClass:
+  def to_dict(self):
+    return asdict(self)
diff --git a/NHentai/entities/doujin.py b/NHentai/entities/doujin.py
index 2780655..210a7f8 100644
--- a/NHentai/entities/doujin.py
+++ b/NHentai/entities/doujin.py
@@ -1,26 +1,166 @@
-from dataclasses import dataclass, field
-from typing import List
+from .base_entity import BaseClass
+from .utils import Mimes
+from dataclasses import dataclass
+from typing import List, Optional
+from urllib.parse import urljoin
+from datetime import datetime
+
+from ..base_wrapper import BaseWrapper
 
 @dataclass
-class Doujin:
-    id: str
-    title: str
-    secondary_title: str
-    tags: List[str]
-    artists: List[str]
-    languages: List[str]
-    categories: List[str]
-    characters: List[str]
-    parodies: List[str]
-    groups: List[str]
-    images: List[str]
-    total_pages: int = 0
+class Title(BaseClass):
+    english: Optional[str]
+    japanese: Optional[str]
+    chinese: Optional[str]
+    pretty: Optional[str]
+
+    @classmethod
+    def from_json(cls, json_object):
+        args = {"english": json_object.get('english'),
+                "japanese": json_object.get('japanese'),
+                "chinese": json_object.get('chinese'),
+                "pretty": json_object.get('pretty')}
+        
+        return cls(*args.values())
+
+@dataclass
+class DoujinPage(BaseClass):
+    index: int
+    media_id: int
+    width: int
+    height: int
+    mime: str
+    src: str
+
+    @classmethod
+    def from_json(cls, json_object: dict, page_index: int, media_id: str=None):
+        args = {"index": page_index+1, 
+                "media_id": media_id, 
+                "width": json_object.get('w'), 
+                "height": json_object.get('h'),
+                "mime": Mimes[json_object.get('t').upper()].value, 
+                "src": urljoin(BaseWrapper._IMAGE_BASE_URL, f'{media_id}/{page_index+1}.{Mimes[json_object.get("t").upper()].value}')}
+        
+        return cls(*args.values())
+
+@dataclass
+class Cover(BaseClass):
+	media_id: int
+	width: int
+	height: int
+	mime: str
+	src: str
+
+	@classmethod
+	def from_json(cls, json_object: dict):
+		args = {"media_id": json_object.get('media_id'),  
+                        "width": json_object.get('w'), 
+                        "height": json_object.get('h'),
+                        "mime": Mimes[json_object.get('t').upper()].value, 
+                        "src": urljoin(BaseWrapper._TINY_IMAGE_BASE_URL, f'{json_object.get("media_id")}/cover.{Mimes[json_object.get("t").upper()].value}')}
+		
+		return cls(*args.values())
+
+@dataclass
+class Thumbnail(BaseClass): ...
+
+@dataclass
+class Tag(BaseClass):
+    id: int
+    type: str
+    name: str
+    url: str
+    count: int
+
+    @classmethod
+    def from_json(cls, json_object: dict):
+
+        args = {"id": json_object.get('id'), 
+                "type": json_object.get('type'), 
+                "name": json_object.get('name'), 
+                "url": urljoin(BaseWrapper._BASE_URL, json_object.get('url')), 
+                "count": json_object.get('count')}
+
+        return cls(*args.values())
 
 @dataclass
-class DoujinThumbnail:
-    id: str
-    title: str
-    lang: str
-    cover: str
+class Doujin(BaseClass):
+    id: int
+    media_id: str
+    upload_at: datetime
     url: str
-    data_tags: list
+    title: List[Title]
+    tags: List[Tag]
+    artists: List[Tag]
+    languages: List[Tag]
+    categories: List[Tag]
+    characters: List[Tag]
+    parodies: List[Tag]
+    groups: List[Tag]
+    cover: Cover
+    images: List[DoujinPage]
+    total_favorites: int = 0
+    total_pages: int = 0
+
+    @classmethod
+    def from_json(cls, json_object: dict):
+
+        ALL_TAGS = json_object.get('tags')
+        MEDIA_ID = json_object.get('media_id')
+
+        TAG_DICT = {'tag': [],
+                    'artist': [],
+                    'group': [],
+                    'parody': [],
+                    'character': [],
+                    'category': [],
+                    'language': []}
+
+        for tag in ALL_TAGS:
+                if TAG_DICT.get(tag.get('type')) is not None:
+                    TAG_DICT[tag.get('type')].append(Tag.from_json(tag))
+
+        COVER = Cover.from_json(json_object={**json_object.get('images').get('cover'), "media_id": MEDIA_ID})
+        PAGES = [DoujinPage.from_json(page, index, MEDIA_ID)
+                 for index, page in enumerate(json_object.get('images').get('pages'))]
+
+        args = {"id": json_object.get('id'),
+                "media_id": json_object.get('media_id'),
+                "upload_at": datetime.fromtimestamp(json_object.get('upload_date')),
+                "url": urljoin(BaseWrapper._BASE_URL, f'g/{json_object.get("id")}'),
+                "title": Title.from_json(json_object.get('title', {})),
+                "tags": [Tag.from_json(tag) for tag in ALL_TAGS],
+                "artists": TAG_DICT['artist'],
+                "languages": TAG_DICT['language'],
+                "categories": TAG_DICT['category'],
+                "characters": TAG_DICT['character'],
+                "parodies": TAG_DICT['parody'],
+                "groups": TAG_DICT['group'],
+                "cover": COVER,
+                "images": PAGES,
+                "total_favorites": int(json_object.get('num_favorites')),
+                "total_pages": len(PAGES)}
+        
+        return cls(*args.values())
+
+@dataclass
+class DoujinThumbnail(BaseClass):
+	id: str
+	media_id: str
+	title: List[Title]
+	languages: List[Tag]
+	cover: Cover
+	url: str
+	tags: List[Tag]
+
+	@classmethod
+	def from_json(cls, json_object: dict):
+                args = {"id": json_object.get('id'), 
+                        "media_id": json_object.get('media_id'), 
+                        "title": Title.from_json(json_object=json_object.get('title')),
+                        "languages":  [Tag.from_json(tag) for tag in json_object.get('tags') if tag.get('type') == 'language'],
+                        "cover": Cover.from_json(json_object={**json_object.get('images').get('cover'), "media_id": json_object.get('media_id')}),
+                        "url": urljoin(BaseWrapper._BASE_URL, f'g/{json_object.get("id")}'),
+                        "tags": [Tag.from_json(tag) for tag in json_object.get('tags')]}
+
+                return cls(*args.values())
diff --git a/NHentai/entities/links.py b/NHentai/entities/links.py
index 60b181d..69a90c7 100644
--- a/NHentai/entities/links.py
+++ b/NHentai/entities/links.py
@@ -1,7 +1,8 @@
 from dataclasses import dataclass
+from .base_entity import BaseClass
 
 @dataclass
-class CharacterLink:
+class CharacterLink(BaseClass):
     section: str
     title: str
     url: str
diff --git a/NHentai/entities/options.py b/NHentai/entities/options.py
new file mode 100644
index 0000000..24f8365
--- /dev/null
+++ b/NHentai/entities/options.py
@@ -0,0 +1,10 @@
+from enum import Enum, unique
+from typing import List, Optional
+
+@unique
+class Sort(Enum):
+    TODAY: str = 'popular-today'
+    WEEK: str = 'popular-week'
+    MONTH: str = 'popular-month'
+    ALL_TIME: str = 'popular'
+    RECENT = None
\ No newline at end of file
diff --git a/NHentai/entities/page.py b/NHentai/entities/page.py
index a412f41..12eb3ce 100644
--- a/NHentai/entities/page.py
+++ b/NHentai/entities/page.py
@@ -1,15 +1,20 @@
+from .base_entity import BaseClass
 from typing import List
 from dataclasses import dataclass
 
-from .doujin import DoujinThumbnail
+from .doujin import Doujin, DoujinThumbnail
+from .links import CharacterLink
 
 @dataclass
-class HomePage:
+class Page(BaseClass):
     doujins: List[DoujinThumbnail]
-    total_pages: int = 0
+    total_results: int
+    total_pages: int
+    per_page: int
+    page: int
 
 @dataclass
-class SearchPage:
+class SearchPage(BaseClass):
     query: str
     sort: str
     total_results: int
@@ -17,30 +22,30 @@ class SearchPage:
     doujins: List[DoujinThumbnail]
 
 @dataclass
-class GroupListPage:
+class GroupListPage(BaseClass):
     page: int
     total_pages: int
     groups: List[str]
 
 @dataclass
-class CharacterListPage:
+class CharacterListPage(BaseClass):
     page: int
     total_pages: int
     characters: List[str]
 
 @dataclass
-class ArtistListPage:
+class ArtistListPage(BaseClass):
     page: int
     total_pages: int
     artists: List[str]
 
 @dataclass
-class TagListPage:
+class TagListPage(BaseClass):
     page: int
     total_pages: int
     tags: List[str]
 
 @dataclass
-class PopularPage:
+class PopularPage(BaseClass):
     doujins: List[DoujinThumbnail]
     total_doujins: int
\ No newline at end of file
diff --git a/NHentai/entities/utils.py b/NHentai/entities/utils.py
new file mode 100644
index 0000000..9ca85ba
--- /dev/null
+++ b/NHentai/entities/utils.py
@@ -0,0 +1,7 @@
+from enum import Enum, unique
+
+@unique
+class Mimes(Enum):
+  J: str = 'jpg'
+  P: str = 'png'
+  G: str = 'gif'
\ No newline at end of file
diff --git a/NHentai/nhentai.py b/NHentai/nhentai.py
index 404f1aa..1c3e89d 100644
--- a/NHentai/nhentai.py
+++ b/NHentai/nhentai.py
@@ -1,138 +1,95 @@
 import logging
+from typing import Optional, Union
+from urllib.parse import urljoin
+from .utils.cache import Cache
 
 from .base_wrapper import BaseWrapper
-from .base_wrapper import (Doujin, 
-                           DoujinThumbnail,
-                           HomePage, 
-                           SearchPage, 
-                           TagListPage, 
-                           GroupListPage, 
-                           CharacterListPage, 
-                           ArtistListPage, 
-                           PopularPage,
-                           CharacterLink) 
+from .entities.doujin import Doujin, DoujinThumbnail
+from .entities.page import (Page, 
+                            SearchPage, 
+                            TagListPage, 
+                            GroupListPage, 
+                            CharacterListPage, 
+                            ArtistListPage, 
+                            PopularPage)
+from .entities.links import CharacterLink 
+from .entities.options import Sort
 
 class NHentai(BaseWrapper):
+    @Cache(max_age_seconds=3600, max_size=1000, cache_key_position=1, cache_key_name='id').cache
     def get_doujin(self, id: str) -> Doujin:
-        """This method receives a string id as parameter, 
-        gets its informations and returns as a Doujin entity.
-        """
-
-        SOUP = self._fetch(f'/g/{id}/')
-
-        info_box = SOUP.find('div', id='info')
+        """This method fetches a doujin information based on id.
 
-        if info_box is None:
-            return None
-
-        title_section = info_box.find('h2', class_='title')
-        
-        TITLE = info_box.find('h1', class_='title').find('span', class_='pretty').text
-        SECONDARY_TITLE = title_section.text if title_section is not None else ''
-        IMAGES = []
-        TAGS_TO_DICT = {}
+        Args:
+            id: 
+                Id of the target doujin.
 
-        # Getting dinamically the doujin properties
-        # This looping may return information as tags, artists, languages, categories and more.
-        tags = info_box.find_all('div', class_='tag-container field-name')
-
-        for item in tags:
-            for i in item.find('span', class_='tags').find_all('a', class_='tag'):
+        Returns:
+            Doujin: 
+                dataclass with the doujin information within.
             
-                if TAGS_TO_DICT.get(item.text.strip().split()[0].lower().replace(':', '')) is None:
-                    TAGS_TO_DICT[item.text.strip().split()[0].lower().replace(':', '')] = []
-
-                TAGS_TO_DICT[item.text.strip().split()[0].lower().replace(':', '')].append(i.find('span', class_='name').text)            
-        
-        # Getting dinamically the doujin images
-        # This looping gets the images id from its thumbs then uses it to create the page final url.
-        thumbs = SOUP.findAll('div', class_='thumb-container')
-
-        for thumb in thumbs:
-            img = thumb.find('img', class_='lazyload')['data-src']
-            img_original_size = img.split('/')
-            gallery_id = img_original_size[4]
-            IMAGES.append(f'{self._IMAGE_BASE_URL}/{gallery_id}/{img_original_size[-1].replace("t", "")}')
-        
-        return Doujin(id=id,
-                      title=TITLE,
-                      secondary_title=SECONDARY_TITLE,
-                      tags=TAGS_TO_DICT.get('tags', []),
-                      artists=TAGS_TO_DICT.get('artists', []),
-                      groups=TAGS_TO_DICT.get('groups', []),
-                      languages=TAGS_TO_DICT.get('languages', []),
-                      categories=TAGS_TO_DICT.get('categories', []),
-                      characters=TAGS_TO_DICT.get('characters', []),
-                      parodies=TAGS_TO_DICT.get('parodies', []),
-                      images=IMAGES,
-                      total_pages=len(IMAGES))
-
-    def get_pages(self, page: int=1) -> HomePage:
-        """This method paginates through the homepage of NHentai and returns the doujins
-        initial information as id, title, language, cover and data-tags.
+            You can access the dataclasses informations at `entities` package.
         """
 
-        SOUP = self._fetch(f'/?page={page}')
+        print(f'INFO::Retrieving doujin with id {id}')
+        id = str(id)
 
-        pagination_section = SOUP.find('section', class_='pagination')
-        TOTAL_PAGES = int(pagination_section.find('a', class_='last')['href'].split('=')[-1])
-        DOUJINS = []
+        if not id.isnumeric() or id[0] == '0':
+            print('ERROR::Maybe you mistyped the doujin id or it doesnt exists.')
+            return None
 
-        doujin_boxes = SOUP.find_all('div', class_='gallery')
-        for item in doujin_boxes:
-            DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(DOUJIN_TITLE)
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-            DOUJIN_TAGS = item['data-tags'].split()
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=DOUJIN_TAGS))
-        
-        return HomePage(doujins=DOUJINS,
-                        total_pages=TOTAL_PAGES)
-    
-    def get_user_page(self, uid: str, username: str) -> dict:
-        
-        """!!!!!DEPRECATED!!!!!
-        """
+        SOUP = self._fetch(urljoin(self._API_URL, f'gallery/{id}'), is_json=True)
 
-        SOUP = self._fetch(f'/users/{uid}/{username}')
+        if SOUP.get('error'):
+            print('ERROR::Maybe you mistyped the doujin id or it doesnt exists.')
+            return None
+         
+        print(f'INFO::Sucessfully retrieved doujin {id}')
 
-        user_container = SOUP.find('div' ,id='user-container')
+        return Doujin.from_json(SOUP)
 
-        profile_pic_url = user_container.find('img')['src']
-        profile_username = user_container.find('h1').text
-        profile_since = user_container.find('time').text
+    @Cache(max_age_seconds=3600, max_size=15, cache_key_position=1, cache_key_name='page').cache
+    def get_pages(self, page: int) -> Page:
+        """This method paginates through the homepage of NHentai and returns the doujins.
 
-        favs_container = SOUP.find('div', id='recent-favorites-container')
+        Args:
+            page: 
+                number of the pagination page.
 
-        galleries = favs_container.find_all('div', class_='gallery')
+        Returns:
+            HomePage: 
+                dataclass with a list of DoujinThumbnail.
+            
+            You can access the dataclasses informations at `entities` package.
+        """
 
-        favs = []
+        print(f'INFO::Fetching page {page}')
+        SOUP = self._fetch(urljoin(self._API_URL, f'galleries/all?page={page}'), is_json=True)
 
-        for gallery in galleries:
-            favs.append({'id': gallery.find('a', class_='cover')['href'].split('/')[2],
-                         'cover': gallery.find('img')['data-src'],
-                         'title': gallery.find('div', class_='caption').text,
-                         'data-tags': gallery['data-tags'].split(' ')})
+        DOUJINS = [DoujinThumbnail.from_json(json_obj) for json_obj in SOUP.get('result')]
+        PAGES = SOUP.get('num_pages')
+        PER_PAGE = SOUP.get('per_page')
+        TOTAL_RESULTS = int(PAGES) * int(PER_PAGE)
 
-        return {
-            'uid': uid,
-            'username': profile_username,
-            'image': profile_pic_url,
-            'since': profile_since,
-            'doujins': favs
-        }
+        return Page(doujins=DOUJINS,
+                    total_results=TOTAL_RESULTS,
+                    total_pages=PAGES,
+                    per_page=PER_PAGE,
+                    page=page)
 
     def get_random(self) -> Doujin:
-        """This method retrieves a random doujin
+        """This method retrieves a random doujin.
+
+        Args:
+
+        Returns:
+            Doujin: 
+                dataclass with the doujin information within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
+
         SOUP = self._fetch(f'/random/')
 
         id = SOUP.find('h3', id='gallery_id').text.replace('#', '')
@@ -141,58 +98,56 @@ def get_random(self) -> Doujin:
             
         return doujin
 
-    def search(self, query: str, sort: str=None, page: int=1) -> SearchPage:
+    def search(self, query: str, page: Optional[int]=1, sort: Optional[Sort]=Sort.RECENT) -> Union[SearchPage, Doujin]:
         """This method retrieves the search page based on a query.
+
+        Args:
+            query str: 
+                searchable term string. Ex: houshou marine, boa hancock, naruto
+            sort str:
+                doujin sort order
+            page int:
+                number of the page with results
+
+        Returns:
+            SearchPage: 
+                dataclass with a list of DoujinThumbnail.
+            
+            You can access the dataclasses informations at `entities` package.
         """
         
+
         if query.isnumeric():
-            any_doujin = self.get_doujin(id=query)
+            any_doujin: Doujin = self.get_doujin(id=query)
             if any_doujin is not None:
                 return any_doujin
 
-        if sort:
-            SOUP = self._fetch(f'/search/?q={query}&page={page}&sort={sort}')
-        else:
-            SOUP = self._fetch(f'/search/?q={query}&page={page}')
-
-        total_results = SOUP.find('div', id='content').find('h1').text.strip().split()[0]
+        sort = sort.value if isinstance(sort, Sort) else sort
+        params = {'query': query, 'page': page, 'sort': sort} if sort is not None else {'query': query, 'page': page}
 
-        TOTAL_RESULTS = int(float(total_results.replace(',', '')))
-        TOTAL_PAGES = 0
-        DOUJINS = []
+        SOUP = self._fetch(urljoin(self._API_URL, f'galleries/search'), params=params, is_json=True)
 
-        pagination_section = SOUP.find('section', class_='pagination')
-        if pagination_section is not None:
-            last_page_HTMLObj = pagination_section.find('a', class_='last')
-            if last_page_HTMLObj is not None:
-                TOTAL_PAGES = int(last_page_HTMLObj['href'].split('&')[1][5:])
-            else:
-                last_page_HTMLObj = pagination_section.find('a', class_='page current')
-                TOTAL_PAGES = int(last_page_HTMLObj['href'].split('&')[1][5:])
-
-        doujin_boxes = SOUP.find_all('div', class_='gallery')
-        for item in doujin_boxes:
-            DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(item.find('div', class_='caption').text)
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-            DOUJIN_TAGS = item['data-tags'].split()
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=DOUJIN_TAGS))
-            
+        DOUJINS = [Doujin.from_json(json_object=doujin) for doujin in SOUP.get('result')]
+        
         return SearchPage(query=query,
-                          sort=sort or 'recente',
-                          total_results=TOTAL_RESULTS,
-                          total_pages=TOTAL_PAGES,
+                          sort=sort,
+                          total_results=SOUP.get('num_pages')*SOUP.get('per_page'),
+                          total_pages=SOUP.get('num_pages'),
                           doujins=DOUJINS)
 
-    def get_characters(self, page: int = 1) -> CharacterListPage:
+    @Cache(max_age_seconds=3600, max_size=15, cache_key_position=1, cache_key_name='page').cache
+    def get_characters(self, page: int) -> CharacterListPage:
         """This method retrieves a list of characters that are available on NHentai site.
+
+        Args:
+            page: 
+                number of the pagination page.
+
+        Returns:
+            CharacterListPage: 
+                dataclass with the character list within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
         SOUP = self._fetch(f'/characters/?page={page}')
@@ -219,7 +174,15 @@ def get_characters(self, page: int = 1) -> CharacterListPage:
                                  characters=CHARACTERS)
     
     def get_popular_now(self):
-        """This method retrieves a list of the current most popular doujins
+        """This method retrieves a list of the current most popular doujins.
+
+        Args:
+
+        Returns:
+            PopularPage: 
+                dataclass with the current popular doujin list within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
         SOUP = self._fetch(f'/')
@@ -228,21 +191,25 @@ def get_popular_now(self):
         DOUJINS = []
 
         for item in popular_section.find_all('div', class_='gallery'):
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(DOUJIN_TITLE)
             DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=item['data-tags'].split()))
+
+            POPULAR_DOUJIN = self.get_doujin(DOUJIN_ID)
+
+            if POPULAR_DOUJIN is not None:
+                DOUJINS.append(DoujinThumbnail(id=POPULAR_DOUJIN.id,
+                                               media_id=POPULAR_DOUJIN.media_id,
+                                               title=POPULAR_DOUJIN.title,
+                                               languages=POPULAR_DOUJIN.languages,
+                                               cover=POPULAR_DOUJIN.cover,
+                                               url=urljoin(self._BASE_URL, f"/g/{POPULAR_DOUJIN.id}"),
+                                               tags=POPULAR_DOUJIN.tags))
         
         return PopularPage(doujins=DOUJINS,
                            total_doujins=len(DOUJINS))
 
+    def get_home_page():
+        raise NotImplementedError 
+
     def get_artists(self, page: int = 1) -> ArtistListPage:
         raise NotImplementedError
 
diff --git a/NHentai/nhentai_async.py b/NHentai/nhentai_async.py
index 4d179e4..7252df8 100644
--- a/NHentai/nhentai_async.py
+++ b/NHentai/nhentai_async.py
@@ -1,138 +1,95 @@
+import asyncio
 import logging
+import asyncio
+from typing import AsyncGenerator
+from urllib.parse import urljoin
 
+from .utils.cache import Cache
 from .base_wrapper import BaseWrapper
-from .base_wrapper import (Doujin, 
-                           DoujinThumbnail,
-                           HomePage, 
-                           SearchPage,
-                           CharacterListPage,
-                           PopularPage,
-                           CharacterLink) 
+from .entities.doujin import Doujin, DoujinThumbnail
+from .entities.page import (Page, 
+                            SearchPage,
+                            CharacterListPage,
+                            PopularPage)
+from .entities.links import CharacterLink 
+from .entities.options import Sort
+
 
 class NHentaiAsync(BaseWrapper):
+    @Cache(max_age_seconds=3600, max_size=1000, cache_key_position=1, cache_key_name='id').async_cache
     async def get_doujin(self, id: str) -> Doujin:
-        """This method receives a string id as parameter, 
-        gets its informations and returns as a Doujin entity.
-        """
-
-        SOUP = await self._async_fetch(f'/g/{id}/')
-
-        info_box = SOUP.find('div', id='info')
+        """This method fetches a doujin information based on id.
 
-        if info_box is None:
-            return None
-
-        title_section = info_box.find('h2', class_='title')
-        
-        TITLE = info_box.find('h1', class_='title').find('span', class_='pretty').text
-        SECONDARY_TITLE: str = title_section.text if title_section is not None else ''
-        IMAGES = list()
-        TAGS_TO_DICT = {}
+        Args:
+            id: 
+                Id of the target doujin.
 
-        # Getting dinamically the doujin properties
-        # This looping may return information as tags, artists, languages, categories and more.
-        tags = info_box.find_all('div', class_='tag-container field-name')
-
-        for item in tags:
-            for i in item.find('span', class_='tags').find_all('a', class_='tag'):
+        Returns:
+            Doujin: 
+                dataclass with the doujin information within.
             
-                if TAGS_TO_DICT.get(item.text.strip().split()[0].lower().replace(':', '')) is None:
-                    TAGS_TO_DICT[item.text.strip().split()[0].lower().replace(':', '')] = []
-
-                TAGS_TO_DICT[item.text.strip().split()[0].lower().replace(':', '')].append(i.find('span', class_='name').text)            
-        
-        # Getting dinamically the doujin images
-        # This looping gets the images id from its thumbs then uses it to create the page final url.
-        thumbs = SOUP.findAll('div', class_='thumb-container')
-
-        for thumb in thumbs:
-            img = thumb.find('img', class_='lazyload')['data-src']
-            img_original_size = img.split('/')
-            gallery_id = img_original_size[4]
-            IMAGES.append(f'{self._IMAGE_BASE_URL}/{gallery_id}/{img_original_size[-1].replace("t", "")}')
-        
-        return Doujin(id=id,
-                      title=TITLE,
-                      secondary_title=SECONDARY_TITLE,
-                      tags=TAGS_TO_DICT.get('tags', []),
-                      artists=TAGS_TO_DICT.get('artists', []),
-                      groups=TAGS_TO_DICT.get('groups', []),
-                      languages=TAGS_TO_DICT.get('languages', []),
-                      categories=TAGS_TO_DICT.get('categories', []),
-                      characters=TAGS_TO_DICT.get('characters', []),
-                      parodies=TAGS_TO_DICT.get('parodies', []),
-                      images=IMAGES,
-                      total_pages=len(IMAGES))
-
-    async def get_pages(self, page: int=1) -> HomePage:
-        """This method paginates through the homepage of NHentai and returns the doujins
-        initial information as id, title, language, cover and data-tags.
+            You can access the dataclasses informations at `entities` package.
         """
 
-        SOUP = await self._async_fetch(f'/?page={page}')
+        print(f'INFO::Retrieving doujin with id {id}')
+        id = str(id)
 
-        pagination_section = SOUP.find('section', class_='pagination')
-        TOTAL_PAGES = int(pagination_section.find('a', class_='last')['href'].split('=')[-1])
-        DOUJINS = []
-
-        doujin_boxes = SOUP.find_all('div', class_='gallery')
-        for item in doujin_boxes:
-            DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(DOUJIN_TITLE)
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-            DOUJIN_TAGS = item['data-tags'].split()
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=DOUJIN_TAGS))
-        
-        return HomePage(doujins=DOUJINS,
-                        total_pages=TOTAL_PAGES)
-    
-    # def get_user_page(self, uid: str, username: str) -> dict:
-        
-    #     """!!!!!DEPRECATED!!!!!
-    #     """
+        if not id.isnumeric() or id[0] == '0':
+            print('ERROR::Maybe you mistyped the doujin id or it doesnt exists.')
+            return None
+
+        SOUP = await self._async_fetch(urljoin(self._API_URL, f'gallery/{id}'), is_json=True)
 
-    #     user_page = requests.get(f'{self._BASE_URL}/users/{uid}/{username}')
-    #     soup = BeautifulSoup(user_page.content, 'html.parser')
+        if SOUP.get('error'):
+            print('ERROR::Maybe you mistyped the doujin id or it doesnt exists.')
+            return None
+         
+        print(f'INFO::Sucessfully retrieved doujin {id}')
 
-    #     user_container = soup.find('div' ,id='user-container')
+        return Doujin.from_json(SOUP)
 
-    #     profile_pic_url = user_container.find('img')['src']
-    #     profile_username = user_container.find('h1').text
-    #     profile_since = user_container.find('time').text
+    @Cache(max_age_seconds=3600, max_size=15, cache_key_position=1, cache_key_name='page').async_cache
+    async def get_pages(self, page: int=1) -> Page:
+        """This method paginates through the homepage of NHentai and returns the doujins.
 
-    #     favs_container = soup.find('div', id='recent-favorites-container')
+        Args:
+            page: 
+                number of the pagination page.
 
-    #     galleries = favs_container.find_all('div', class_='gallery')
+        Returns:
+            HomePage: 
+                dataclass with a list of DoujinThumbnail.
+            
+            You can access the dataclasses informations at `entities` package.
+        """
 
-    #     favs = []
+        print(f'INFO::Fetching page {page}')
+        SOUP = await self._async_fetch(urljoin(self._API_URL, f'galleries/all?page={page}'), is_json=True)
 
-    #     for gallery in galleries:
-    #         favs.append({
-    #             'id': gallery.find('a', class_='cover')['href'].split('/')[2],
-    #             'cover': gallery.find('img')['data-src'],
-    #             'title': gallery.find('div', class_='caption').text,
-    #             'data-tags': gallery['data-tags'].split(' ')
-    #         })
+        DOUJINS = [DoujinThumbnail.from_json(json_obj) for json_obj in SOUP.get('result')]
+        PAGES = SOUP.get('num_pages')
+        PER_PAGE = SOUP.get('per_page')
+        TOTAL_RESULTS = int(PAGES) * int(PER_PAGE)
 
-    #     return {
-    #         'uid': uid,
-    #         'username': profile_username,
-    #         'image': profile_pic_url,
-    #         'since': profile_since,
-    #         'doujins': favs
-    #     }
+        return Page(doujins=DOUJINS,
+                    total_results=TOTAL_RESULTS,
+                    total_pages=PAGES,
+                    per_page=PER_PAGE,
+                    page=int(page))
 
     async def get_random(self) -> Doujin:
-        """This method retrieves a random doujin
+        """This method retrieves a random doujin.
+
+        Args:
+
+        Returns:
+            Doujin: 
+                dataclass with the doujin information within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
+
         SOUP = await self._async_fetch(f'/random/')
 
         id = SOUP.find('h3', id='gallery_id').text.replace('#', '')
@@ -141,65 +98,81 @@ async def get_random(self) -> Doujin:
             
         return doujin
 
-    async def search(self, query: str, sort: str=None, page: int=1) -> SearchPage:
+    async def search(self, query: str, sort: str=Sort.RECENT, page: int=1) -> SearchPage:
         """This method retrieves the search page based on a query.
+
+        Args:
+            query str: 
+                searchable term string. Ex: houshou marine, boa hancock, naruto
+            sort str:
+                doujin sort order
+            page int:
+                number of the page with results
+
+        Returns:
+            SearchPage: 
+                dataclass with a list of DoujinThumbnail.
+            
+            You can access the dataclasses informations at `entities` package.
         """
-        
+
         if query.isnumeric():
-            any_doujin = await self.get_doujin(id=query)
+            any_doujin: Doujin = await self.get_doujin(id=query)
             if any_doujin is not None:
                 return any_doujin
 
-        soup = None
+        sort = sort.value if isinstance(sort, Sort) else sort
+        params = {'query': query, 'page': page, 'sort': sort} if sort is not None else {'query': query, 'page': page}
 
-        if sort:
-            soup = await self._async_fetch(f'/search/?q={query}&page={page}&sort={sort}')
-        else:
-            soup = await self._async_fetch(f'/search/?q={query}&page={page}')
+        SOUP = await self._async_fetch(urljoin(self._API_URL, f'galleries/search'), params=params, is_json=True)
 
-        total_results = soup.find('div', id='content').find('h1').text.strip().split()[0]
-
-        TOTAL_RESULTS = int(float(total_results.replace(',', '')))
-        TOTAL_PAGES = 0
-        DOUJINS = []
-
-        pagination_section = soup.find('section', class_='pagination')
-        if pagination_section is not None:
-            last_page_HTMLObj = pagination_section.find('a', class_='last')
-            if last_page_HTMLObj is not None:
-                TOTAL_PAGES = int(last_page_HTMLObj['href'].split('&')[1][5:])
-            else:
-                last_page_HTMLObj = pagination_section.find('a', class_='page current')
-                TOTAL_PAGES = int(last_page_HTMLObj['href'].split('&')[1][5:])
-
-        doujin_boxes = soup.find_all('div', class_='gallery')
-        for item in doujin_boxes:
-
-            DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(item.find('div', class_='caption').text)
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-            DOUJIN_TAGS = item['data-tags'].split()
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=DOUJIN_TAGS))
-            
+        DOUJINS = [Doujin.from_json(json_object=doujin) for doujin in SOUP.get('result')]
+        
         return SearchPage(query=query,
-                          sort=sort or 'recente',
-                          total_results=TOTAL_RESULTS,
-                          total_pages=TOTAL_PAGES,
+                          sort=sort,
+                          total_results=SOUP.get('num_pages')*SOUP.get('per_page'),
+                          total_pages=SOUP.get('num_pages'),
                           doujins=DOUJINS)
-
-    async def get_characters(self, page: int = 1) -> CharacterListPage:
+    
+    async def search_pages(self,
+                           query: str,
+                           sort: str=Sort.RECENT,
+                           max_pages: int=1,
+                           *, concurrent_tasks: int=3) -> AsyncGenerator[SearchPage, None]:
+        TASKS = []
+        
+        for page in range(1, max_pages + 1):
+            if len(TASKS) < concurrent_tasks:
+                task = asyncio.ensure_future(self.search(query=query, sort=sort, page=page))
+                TASKS.append(task)
+            
+            else:
+                for task in TASKS:
+                    yield await task
+                
+                TASKS = []
+        
+        # yield remaining tasks
+        for task in TASKS:
+            yield await task
+    
+    @Cache(max_age_seconds=3600, max_size=15, cache_key_position=1, cache_key_name='page').async_cache
+    async def get_characters(self, page) -> CharacterListPage:
         """This method retrieves a list of characters that are available on NHentai site.
+
+        Args:
+            page: 
+                number of the pagination page.
+
+        Returns:
+            CharacterListPage: 
+                dataclass with the character list within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
         SOUP = await self._async_fetch(f'/characters/?page={page}')
-
+        
         pagination_section = SOUP.find('section', class_='pagination')
         TOTAL_PAGES = int(pagination_section.find('a', class_='last')['href'].split('=')[-1])
         CHARACTERS = []
@@ -222,29 +195,39 @@ async def get_characters(self, page: int = 1) -> CharacterListPage:
                                  characters=CHARACTERS)
 
     async def get_popular_now(self):
-        """This method retrieves a list of the current most popular doujins
+        """This method retrieves a list of the current most popular doujins.
+
+        Args:
+
+        Returns:
+            PopularPage: 
+                dataclass with the current popular doujin list within.
+            
+            You can access the dataclasses informations at `entities` package.
         """
 
         SOUP = await self._async_fetch(f'/')
         
         popular_section = SOUP.find('div', class_='index-popular')
-        DOUJINS = []
-
-        for item in popular_section.find_all('div', class_='gallery'):
-            DOUJIN_TITLE = item.find('div', class_='caption').text
-            DOUJIN_LANG = self._get_lang_by_title(DOUJIN_TITLE)
-            DOUJIN_ID = item.find('a', class_='cover')['href'].split('/')[2]
-            DOUJIN_COVER = item.find('img', class_='lazyload')['data-src']
-
-            DOUJINS.append(DoujinThumbnail(id=DOUJIN_ID,
-                                           title=DOUJIN_TITLE,
-                                           lang=DOUJIN_LANG,
-                                           cover=DOUJIN_COVER,
-                                           url=f"{self._BASE_URL}/g/{DOUJIN_ID}",
-                                           data_tags=item['data-tags'].split()))
+
+        DOUJINS_IDS = [item.find('a', class_='cover')['href'].split('/')[2] for item in popular_section.find_all('div', class_='gallery')]
+        ROUTINES = [self.get_doujin(doujin_id) for doujin_id in DOUJINS_IDS]
+
+        DOUJINS = await asyncio.gather(*ROUTINES)
+        DOUJIN_LIST = list()
+
+        for popular_doujin in DOUJINS:
+            if popular_doujin is not None:
+                DOUJIN_LIST.append(DoujinThumbnail(id=popular_doujin.id,
+                                                media_id=popular_doujin.media_id,
+                                                title=popular_doujin.title,
+                                                languages=popular_doujin.languages,
+                                                cover=popular_doujin.cover,
+                                                url=urljoin(self._BASE_URL, f"/g/{popular_doujin.id}"),
+                                                tags=popular_doujin.tags))
         
-        return PopularPage(doujins=DOUJINS,
-                           total_doujins=len(DOUJINS))
+        return PopularPage(doujins=DOUJIN_LIST,
+                           total_doujins=len(DOUJIN_LIST))
 
     # def get_artists(self, page: int = 1) -> ArtistListPage:
     #     raise NotImplementedError
diff --git a/NHentai/utils/__init__.py b/NHentai/utils/__init__.py
new file mode 100644
index 0000000..371ae6a
--- /dev/null
+++ b/NHentai/utils/__init__.py
@@ -0,0 +1,1 @@
+from . import cache
\ No newline at end of file
diff --git a/NHentai/utils/cache.py b/NHentai/utils/cache.py
new file mode 100644
index 0000000..ce83220
--- /dev/null
+++ b/NHentai/utils/cache.py
@@ -0,0 +1,44 @@
+import functools
+from expiringdict import ExpiringDict
+from typing import Callable
+
+class Cache:
+  _CACHE = None
+
+  def __init__(self, cache_key_position: int, cache_key_name: str, max_age_seconds: int=3600, max_size: int=100):
+    self._CACHE = ExpiringDict(max_len=max_size, max_age_seconds=max_age_seconds, items=None)
+    self.cache_key_position = cache_key_position
+    self.cache_key_name = cache_key_name
+  
+  def cache(self, function: Callable):
+    def wrapper(*args, **kwargs):
+
+      key = kwargs.get(self.cache_key_name) if kwargs.get(self.cache_key_name) is not None else args[self.cache_key_position]
+
+      if self._CACHE.get(key) is not None:
+        print(f'INFO::retrieving cached object with key {key}')
+        return self._CACHE.get(key)
+      else:
+        new_execution = function(*args, **kwargs)
+        self._CACHE[key] = new_execution
+        return new_execution
+        
+    return wrapper
+  
+  def async_cache(self, function: Callable):
+    @functools.wraps(function)
+    async def wrapper(*args, **kwargs):
+      
+      key = kwargs.get(self.cache_key_name) if kwargs.get(self.cache_key_name) is not None else args[self.cache_key_position]
+
+      if self._CACHE.get(key) is not None:
+        print(f'INFO::retrieving cached object with key {key}')
+        return self._CACHE.get(key)
+      else:
+        new_execution = await function(*args, **kwargs)
+        self._CACHE[key] = new_execution
+        return new_execution
+
+    return wrapper
+
+
diff --git a/README.md b/README.md
index b0259fd..49fba8a 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,24 @@
+[![CodeFactor](https://www.codefactor.io/repository/github/alexandresenpai/nhentai-api/badge)](https://www.codefactor.io/repository/github/alexandresenpai/nhentai-api)
+[![PyPI download month](https://img.shields.io/pypi/dm/NHentai-API.svg)](https://pypi.python.org/pypi/NHentai-API/)
+[![codecov](https://codecov.io/gh/AlexandreSenpai/NHentai-API/branch/dev/graph/badge.svg?token=F3LP15DYMR)](https://codecov.io/gh/AlexandreSenpai/NHentai-API)
+[![Python 3.7+](https://img.shields.io/badge/Python-3.7+-blue.svg)](https://www.python.org/downloads/release/python-370/)
+[![PyPI license](https://img.shields.io/pypi/l/ansicolortags.svg)](https://pypi.python.org/pypi/ansicolortags/)
+
 # NHentai API
-A NHentai API made using python webscrapping
-For update notes follow me on [Twitter](https://twitter.com/AlexandreSenpa1).
 
-### Instalation
+A NHentai API made using python webscrapping. \
+For update notes follow me on [Twitter](https://twitter.com/AlexandreSenpa1) or join on NHentai-API [discord server](https://discord.gg/576uSRDD)
+
+### Installation
 ```bash
- pip install --upgrade NHentai-API
- # or pip3 install --upgrade NHentai-API
+pip install --upgrade NHentai-API
+```
+or
+```
+pip3 install --upgrade NHentai-API
 ```
-### Library Features
 
+### Library Features
 - Home page pagination,
 - Doujin information,
 - Random doujin,
@@ -16,168 +26,189 @@ For update notes follow me on [Twitter](https://twitter.com/AlexandreSenpa1).
 - Character List
 - Popular List
 
-### Usage
-
-##### Home
+### Home
 
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    random_doujin: HomePage = nhentai.get_pages(page=1)
+nhentai = NHentai()
+random_doujin: HomePage = nhentai.get_pages(page=1)
 ```
-
-the expected output is a HomePage instance:
+The expected output is a HomePage instance:
 ```python
-    HomePage(doujins: [DoujinThumbnail(id: str,
-                                       title: str,
-                                       lang: str,
-                                       cover: str,
-                                       url: str,
-                                       data_tags: List[str])], 
-             total_pages: int)
+Page(
+    doujins: List[
+        DoujinThumbnail(
+	        id: str
+	        media_id: str
+	        title: List[Title]
+	        languages: List[Tag]
+	        cover: Cover
+	        url: str
+	        tags: List[Tag]
+        )
+    ],
+
+    total_pages: int,
+    total_results: int,
+    per_page: int)
 ```
 
-##### Random
-
+### Random
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    random_doujin: Doujin = nhentai.get_random()
+nhentai = NHentai()
+random_doujin: Doujin = nhentai.get_random()
 ```
-
 The expected output is a Doujin instance:
 ```python
-    Doujin(id: str
-           title: str
-           secondary_title: str
-           tags: List[str]
-           artists: List[str]
-           languages: List[str]
-           categories: List[str]
-           characters: List[str]
-           parodies: List[str]
-           groups: List[str]
-           images: List[str]
-           total_pages: int)
+Doujin(
+    id: int
+    media_id: str
+    upload_at: datetime
+    url: str
+    title: List[Title]
+    tags: List[Tag]
+    artists: List[Tag]
+    languages: List[Tag]
+    categories: List[Tag]
+    characters: List[Tag]
+    parodies: List[Tag]
+    groups: List[Tag]
+    cover: Cover
+    images: List[DoujinPage]
+    total_favorites: int = 0
+    total_pages: int = 0
+)
 ```
+Note: Not all doujins have certain properties like `tags`, `artists`, etc. They could be an empty list or a NoneType value.
 
-It's good always remember that some doujins doesnt have many properties that are listed above like artists, characters, parodies and more. This is only the default Doujin dataclass template.
-
-##### Search
-
+### Search
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    search_obj: SearchPage = nhentai.search(query='naruto', sort='popular', page=1)
-    search_obj: SearchPage = nhentai.search(query='30955', sort='popular', page=1)
+nhentai = NHentai()
+search_obj: SearchPage = nhentai.search(query='naruto', sort=Sort.TODAY, page=1)
+search_obj: SearchPage = nhentai.search(query='30955', page=1)
 ```
-
-expected output:
+The expected output is a SearchPage instance:
 ```python
-    SearchPage(query: str, 
-               sort: str, 
-               total_results: int, 
-               doujins: [DoujinThumbnail(id: str,
-                                         title: str, 
-                                         lang: str, 
-                                         cover: str,
-                                         url: str,
-                                         data_tags: List[str])], 
-               total_pages: int)
+SearchPage(
+    query: str
+    sort: str
+    total_results: int
+    total_pages: int
+    doujins: List[
+        DoujinThumbnail(
+	        id: str
+	        media_id: str
+	        title: List[Title]
+	        languages: List[Tag]
+	        cover: Cover
+	        url: str
+	        tags: List[Tag]
+        )
+    ]
+)
 ```
 
-##### Doujin
-
+### Doujin
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    doujin: Doujin = nhentai.get_doujin(id='287167')
+nhentai = NHentai()
+doujin: Doujin = nhentai.get_doujin(id='287167')
 ```
-
-expected output:
+The expected output is a Doujin instance:
 ```python
-    Doujin(id: str
-           title: str
-           secondary_title: str
-           tags: List[str]
-           artists: List[str]
-           languages: List[str]
-           categories: List[str]
-           characters: List[str]
-           parodies: List[str]
-           groups: List[str]
-           images: List[str]
-           total_pages: int)
-```
-
-##### Characters
+Doujin(
+    id: int
+    media_id: str
+    upload_at: datetime
+    url: str
+    title: List[Title]
+    tags: List[Tag]
+    artists: List[Tag]
+    languages: List[Tag]
+    categories: List[Tag]
+    characters: List[Tag]
+    parodies: List[Tag]
+    groups: List[Tag]
+    cover: Cover
+    images: List[DoujinPage]
+    total_favorites: int = 0
+    total_pages: int = 0
+)
+```
 
+### Characters
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    doujin: CharacterListPage = nhentai.get_characters(page=1)
+nhentai = NHentai()
+doujin: CharacterListPage = nhentai.get_characters(page=1)
 ```
-
-expected output:
+The expected output is a CharacterListPage instance:
 ```python
-    CharacterListPage(page=int,
-                      total_pages=int,
-                      characters=[CharacterLink(section: str
-                                                title: str
-                                                url: str
-                                                total_entries: int)])
+CharacterListPage(
+    page: int
+    total_pages: int
+    characters: List[
+        CharacterLink(
+            section: str
+            title: str
+            url: str
+            total_entries: int
+        )
+    ]
+)
 ```
 
-##### Most Popular
-
+### Most Popular
 ```python
-from NHentai.nhentai import NHentai
+from NHentai import NHentai
 
-if __name__ == '__main__':
-    nhentai = NHentai()
-    doujins: PopularPage = nhentai.get_popular_now()
+nhentai = NHentai()
+doujins: PopularPage = nhentai.get_popular_now()
 ```
-
-expected output:
+The expected output is a PopularPage instance:
 ```python
-    PopularPage(doujins=List[DoujinThumbnail(id: str,
-                                             title: str, 
-                                             lang: str, 
-                                             cover: str,
-                                             url: str,
-                                             data_tags: List[str])],
-                total_doujins: int)
+PopularPage(
+    total_doujins: int
+    doujins: List[
+        DoujinThumbnail(
+	        id: str
+	        media_id: str
+	        title: List[Title]
+	        languages: List[Tag]
+	        cover: Cover
+	        url: str
+	        tags: List[Tag]
+        )
+    ],
+)
 ```
 
-## Introducing NHentai Async
-This is the first version of the asynchronous nhentai scrapper. The methods works in the very same way as the base nhentai scrapper, but to make it works you'll have to work with asyncio module using an event loop that you can import from it or get from NHentaiAsync class property: `event_loop`.
+# NHentai API Async
+
+This is the first version of the asynchronous nhentai scrapper. The methods work in the very same way as the base nhentai scrapper, but to make it work you'll have to work with asyncio module using an event loop that you can import from it or get from NHentaiAsync class property: `event_loop`.
 
 Since we're working with async functions, you can only call the NHentaiAsync methods from inside an async funcion or context.
+If you are already working in an async event loop, such as a python Discord API like `discord.py`, you can simply await calls that you would otherwise have to call `run_until_complete` on top of.
 
+Async example 1:
 ```py
-from NHentai.nhentai_async import NHentaiAsync 
+from NHentai import NHentaiAsync
 
-if __name__ == '__main__':
-    nhentai_async = NHentaiAsync()
-    event_loop = nhentai_async.event_loop
-    popular = event_loop.run_until_complete(nhentai_async.get_popular_now())
-    print(popular)
+nhentai_async = NHentaiAsync()
+event_loop = nhentai_async.event_loop
+popular = event_loop.run_until_complete(nhentai_async.get_popular_now())
+print(popular)
 ```
-
-or even
-
+Async example 2:
 ```python
-from NHentai.nhentai_async import NHentaiAsync 
+from NHentai import NHentaiAsync
 
 nhentai_async = NHentaiAsync()
 
@@ -185,7 +216,17 @@ async def get_popular():
     popular = await nhentai_async.get_popular_now()
     print(popular)
 
-if __name__ == '__main__':
-    event_loop = nhentai_async.event_loop
-    event_loop.run_until_complete(get_popular())
-```
\ No newline at end of file
+event_loop = nhentai_async.event_loop
+event_loop.run_until_complete(get_popular())
+```
+
+Await example:
+```python
+from NHentai import NHentaiAsync
+nhentai_async = NHentaiAsync()
+
+# Run in an async function or you will get an error: `'await' outside async function`.
+popular = await nhentai_async.get_popular_now()
+print(popular)
+```
+
diff --git a/requirements.txt b/requirements.txt
index 3b37212..ef9f8f1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
 requests==2.25.0
 beautifulsoup4==4.9.3
 pytest==6.2.4
-aiohttp==3.7.4.post0
\ No newline at end of file
+aiohttp==3.7.4.post0
+pytest-asyncio==0.15.1
+expiringdict==1.2.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 568216d..958e265 100644
--- a/setup.py
+++ b/setup.py
@@ -1,27 +1,53 @@
 from setuptools import setup
+import os
 
 def readme():
     with open('README.md') as f:
         return f.read()
 
-setup(
-    name='NHentai-API',
-    version='0.0.16',
-    description='NHentai Python API made using webscraping.',
-    long_description=readme(),
-    long_description_content_type='text/markdown',
-    classifiers=[
-        "Programming Language :: Python :: 3",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-    ],
-    url='https://github.com/AlexandreSenpai/NHentai-API',
-    author='AlexandreSenpai',
-    author_email='alexandrebsramos@hotmail.com',
-    keywords='Tagshentai, nhentai, nhentai.net, API, NSFW',
-    license='MIT',
-    packages=['NHentai', 'NHentai.entities'],
-    install_requires=['requests', 'beautifulsoup4', 'aiohttp'],
-    include_package_data=True,
-    zip_safe=False
-)
\ No newline at end of file
+env = os.getenv('environment_name') or 'dev'
+
+if env == 'prd':
+    setup(
+        name='NHentai-API',
+        version='0.0.17',
+        description='NHentai Python API made using webscraping.',
+        long_description=readme(),
+        long_description_content_type='text/markdown',
+        classifiers=[
+            "Programming Language :: Python :: 3",
+            "License :: OSI Approved :: MIT License",
+            "Operating System :: OS Independent",
+        ],
+        url='https://github.com/AlexandreSenpai/NHentai-API',
+        author='AlexandreSenpai',
+        author_email='alexandrebsramos@hotmail.com',
+        keywords='Tagshentai, nhentai, nhentai.net, API, NSFW',
+        license='MIT',
+        packages=['NHentai', 'NHentai.entities', 'NHentai.utils'],
+        install_requires=['requests', 'beautifulsoup4', 'aiohttp', 'expiringdict'],
+        include_package_data=True,
+        zip_safe=False
+    )
+elif env == 'dev':
+    setup(
+        name='dev-nhentai-build',
+        version='0.0.6',
+        description='NHentai Python API made using webscraping.',
+        long_description=readme(),
+        long_description_content_type='text/markdown',
+        classifiers=[
+            "Programming Language :: Python :: 3",
+            "License :: OSI Approved :: MIT License",
+            "Operating System :: OS Independent",
+        ],
+        url='https://github.com/AlexandreSenpai/NHentai-API',
+        author='AlexandreSenpai',
+        author_email='alexandrebsramos@hotmail.com',
+        keywords='Tagshentai, nhentai, nhentai.net, API, NSFW',
+        license='MIT',
+        packages=['dev_nhentai', 'dev_nhentai.entities', 'dev_nhentai.utils'],
+        install_requires=['requests', 'beautifulsoup4', 'aiohttp', 'expiringdict'],
+        include_package_data=True,
+        zip_safe=False
+    )
\ No newline at end of file


Test patch:
diff --git a/tests/test_caching.py b/tests/test_caching.py
new file mode 100644
index 0000000..e84e6b6
--- /dev/null
+++ b/tests/test_caching.py
@@ -0,0 +1,70 @@
+import asyncio
+import random
+import sys
+import os
+import time
+
+import pytest
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from NHentai import NHentai
+from NHentai import NHentaiAsync
+
+def test_if_returned_doujin_is_in_cache():
+  s = time.time()
+  NHentai().get_doujin('320165')
+  reference_time_took = time.time() - s
+
+  times = list()
+
+  for _ in range(5):
+    s = time.time()
+    NHentai().get_doujin('320165')
+    times.append(time.time() - s)
+
+  s = time.time()
+  NHentai().get_doujin('320164')
+  not_cached_time_took = time.time() - s
+
+  for _ in range(5):
+    s = time.time()
+    NHentai().get_doujin('320164')
+    times.append(time.time() - s)
+
+  NHentai().get_doujin('320165')
+
+
+  assert False not in [reference_time_took > execution_time and not_cached_time_took > execution_time for execution_time in times]
+
+def test_if_cache_max_len_is_working():
+  s = time.time()
+  NHentai().get_pages(page=1)
+  final_first_page = time.time() - s
+  s = time.time()
+  NHentai().get_pages(1)
+  final_first_page_cached = time.time() - s
+  NHentai().get_pages(page=2)
+  NHentai().get_pages(3)
+  NHentai().get_pages(4)
+  NHentai().get_pages(5)
+  s = time.time()
+  NHentai().get_pages(6)
+  NHentai().get_pages(1)
+  final_second_fetch_of_first_page = time.time() - s
+
+  assert final_first_page > final_first_page_cached and final_second_fetch_of_first_page > final_first_page_cached
+
+@pytest.mark.asyncio
+async def test_async_caching_system():
+  async def fetch_sleeping(id):
+    await asyncio.sleep(random.randint(0, 10))
+    return await NHentaiAsync().get_doujin(id)
+
+  dummy = await NHentaiAsync().get_doujin('320164')
+
+  tasks = [fetch_sleeping('320164') for _ in range(5)]
+
+  res = await asyncio.gather(*tasks)
+
+  assert len(res) == 5
\ No newline at end of file
diff --git a/tests/test_doujin.py b/tests/test_doujin.py
index 258d9d7..29f5c16 100644
--- a/tests/test_doujin.py
+++ b/tests/test_doujin.py
@@ -1,3 +1,4 @@
+from datetime import datetime
 import sys
 import os
 
@@ -5,64 +6,127 @@
 
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
-from NHentai.nhentai import NHentai
-from NHentai.nhentai_async import NHentaiAsync
+from NHentai import NHentai
+from NHentai import NHentaiAsync
+from NHentai.entities.doujin import Doujin, DoujinPage, Tag, Title
 
-def test_standard_case01():
+def test_standard_specting_doujin_00():
     doujin = NHentai().get_doujin('320165')
-    assert doujin is not None
 
-def test_standard_case02():
-    doujin = NHentai().get_doujin('320066')
-    assert doujin is not None
-
-def test_standard_case03():
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
+
+def test_standard_specting_doujin_02():
     doujin = NHentai().get_doujin('320067')
-    assert doujin is not None
 
-def test_standard_case04():
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
+
+def test_standard_specting_doujin_03():
+    doujin = NHentai().get_doujin('320290')
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
+
+def test_if_all_required_keys_arent_none():
+    doujin = NHentai().get_doujin('320290')
+
+    assert doujin.id is not None
+    assert doujin.media_id is not None
+    assert doujin.cover.media_id is not None
+
+def test_standard_not_specting_doujin_00():
     doujin = NHentai().get_doujin('0656654654')
     assert doujin is None
 
-def test_standard_case05():
+def test_standard_not_specting_doujin_01():
     doujin = NHentai().get_doujin('1212145487')
     assert doujin is None
 
-def test_standard_case06():
-    doujin = NHentai().get_doujin('320290')
-    assert doujin is not None
-
-def test_standard_case07():
+def test_standard_not_specting_doujin_02():
     doujin = NHentai().get_doujin('full color')
     assert doujin is None
 
-def test_standard_case08():
-    doujin = NHentai().get_doujin('342490')
-    assert doujin.artists is not None
-    assert doujin.categories is not None
-    assert doujin.groups is not None
-    assert doujin.id is not None
-    assert doujin.images is not None
-    assert doujin.languages is not None
-    assert doujin.parodies is not None
-    assert doujin.secondary_title is not None
-    assert doujin.title is not None
-    assert doujin.total_pages is not None
-
 @pytest.mark.asyncio
 async def test_async_case01():
     doujin = await NHentaiAsync().get_doujin('320165')
-    assert doujin is not None
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
 
 @pytest.mark.asyncio
 async def test_async_case02():
     doujin = await NHentaiAsync().get_doujin('320066')
-    assert doujin is not None
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
 
 @pytest.mark.asyncio
 async def test_async_case03():
     doujin = await NHentaiAsync().get_doujin('320067')
-    assert doujin is not None
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
 
 @pytest.mark.asyncio
 async def test_async_case04():
@@ -77,7 +141,19 @@ async def test_async_case05():
 @pytest.mark.asyncio
 async def test_async_case06():
     doujin = await NHentaiAsync().get_doujin('320290')
-    assert doujin is not None
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
 
 @pytest.mark.asyncio
 async def test_async_case07():
@@ -87,13 +163,16 @@ async def test_async_case07():
 @pytest.mark.asyncio
 async def test_async_case08():
     doujin = await NHentaiAsync().get_doujin('342490')
-    assert doujin.artists is not None
-    assert doujin.categories is not None
-    assert doujin.groups is not None
-    assert doujin.id is not None
-    assert doujin.images is not None
-    assert doujin.languages is not None
-    assert doujin.parodies is not None
-    assert doujin.secondary_title is not None
-    assert doujin.title is not None
-    assert doujin.total_pages is not None
\ No newline at end of file
+
+    assert doujin.artists is not None and False not in [isinstance(tag, Tag) for tag in doujin.artists]
+    assert doujin.upload_at is not None and isinstance(doujin.upload_at, datetime)
+    assert doujin.categories is not None and False not in [isinstance(tag, Tag) for tag in doujin.categories]
+    assert doujin.groups is not None and False not in [isinstance(tag, Tag) for tag in doujin.groups]
+    assert doujin.id is not None and isinstance(doujin.id, int)
+    assert doujin.media_id is not None
+    assert doujin.images is not None and False not in [isinstance(image, DoujinPage) for image in doujin.images]
+    assert doujin.languages is not None and False not in [isinstance(tag, Tag) for tag in doujin.languages]
+    assert doujin.parodies is not None and False not in [isinstance(tag, Tag) for tag in doujin.parodies]
+    assert doujin.title is not None and isinstance(doujin.title, Title)
+    assert doujin.total_pages is not None and isinstance(doujin.total_pages, int)
+    assert doujin is not None and isinstance(doujin, Doujin)
\ No newline at end of file
diff --git a/tests/test_most_popular.py b/tests/test_most_popular.py
index c5ec0b5..9d5e8a2 100644
--- a/tests/test_most_popular.py
+++ b/tests/test_most_popular.py
@@ -5,37 +5,51 @@
 
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
-from NHentai.nhentai import NHentai
-from NHentai.nhentai_async import NHentaiAsync
+from NHentai import NHentai
+from NHentai import NHentaiAsync
 from NHentai.entities.doujin import DoujinThumbnail
 from NHentai.entities.page import PopularPage
 
-def test_standard_case01():
+def test_if_returned_payload_has_all_required_keys():
     doujin = NHentai().get_popular_now()
-    assert doujin is not None
-
-def test_standard_case02():
+    for d in doujin.doujins:
+        assert d.id is not None
+        assert d.cover is not None
+        assert d.url is not None
+        assert d.tags is not None
+        assert d.media_id is not None
+        assert d.title is not None
+        assert d.languages is not None
+
+def test_if_returned_doujin_instance_belongs_to_DoujinThumbnail():
     doujins = NHentai().get_popular_now()
     for doujin in doujins.doujins:
         assert isinstance(doujin, DoujinThumbnail)
 
-def test_standard_case03():
+def test_if_returned_instance_belongs_to_PopularPage():
     doujin = NHentai().get_popular_now()
     assert isinstance(doujin, PopularPage)
 
 @pytest.mark.asyncio
 async def test_async_case01():
     doujin = await NHentaiAsync().get_popular_now()
-    assert doujin is not None
-
-@pytest.mark.asyncio
-async def test_async_case02():
-    doujins = await NHentaiAsync().get_popular_now()
-    for doujin in doujins.doujins:
-        assert isinstance(doujin, DoujinThumbnail)
-
-@pytest.mark.asyncio
-async def test_async_case03():
-    doujin = await NHentaiAsync().get_popular_now()
-    assert isinstance(doujin, PopularPage)
+    for d in doujin.doujins:
+        assert d.id is not None
+        assert d.cover is not None
+        assert d.url is not None
+        assert d.tags is not None
+        assert d.media_id is not None
+        assert d.title is not None
+        assert d.languages is not None
+
+# @pytest.mark.asyncio
+# async def test_async_case02():
+#     doujins = await NHentaiAsync().get_popular_now()
+#     for doujin in doujins.doujins:
+#         assert isinstance(doujin, DoujinThumbnail)
+
+# @pytest.mark.asyncio
+# async def test_async_case03():
+#     doujin = await NHentaiAsync().get_popular_now()
+#     assert isinstance(doujin, PopularPage)
 
diff --git a/tests/test_pages.py b/tests/test_pages.py
index b8bd7ad..a1b0ee6 100644
--- a/tests/test_pages.py
+++ b/tests/test_pages.py
@@ -5,23 +5,23 @@
 
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
-from NHentai.nhentai import NHentai
-from NHentai.nhentai_async import NHentaiAsync
+from NHentai import NHentai
+from NHentai import NHentaiAsync
 
 def test_standard_payload_integrity_home_page():
-    pages = NHentai().get_pages()
+    pages = NHentai().get_pages(page=1)
 
     doujins = pages.doujins
 
     for doujin in doujins:
         assert doujin.id is not None 
         assert doujin.title is not None 
-        assert doujin.lang is not None 
+        assert doujin.languages is not None 
         assert doujin.cover is not None 
-        assert doujin.data_tags is not None 
+        assert doujin.tags is not None 
 
 def test_standard_payload_integrity_characters_page():
-    chars = NHentai().get_characters()
+    chars = NHentai().get_characters(page=1)
     assert chars.page is not None and isinstance(chars.page, int)
     assert chars.total_pages is not None and isinstance(chars.total_pages, int)
     assert chars.characters is not None and isinstance(chars.characters, list)
@@ -31,22 +31,30 @@ def test_standard_payload_integrity_characters_page():
         assert char.url is not None and isinstance(char.url, str)
         assert char.total_entries is not None and isinstance(char.total_entries, int)
 
+def test_if_all_required_keys_arent_none():
+    doujins = NHentai().get_pages(1)
+
+    for doujin in doujins.doujins:
+        assert doujin.id is not None
+        assert doujin.media_id is not None
+        assert doujin.cover.media_id is not None
+
 @pytest.mark.asyncio
 async def test_async_payload_integrity_home_page():
-    pages = await NHentaiAsync().get_pages()
+    pages = await NHentaiAsync().get_pages(page=1)
 
     doujins = pages.doujins
 
     for doujin in doujins:
         assert doujin.id is not None 
         assert doujin.title is not None 
-        assert doujin.lang is not None 
+        assert doujin.languages is not None 
         assert doujin.cover is not None 
-        assert doujin.data_tags is not None 
+        assert doujin.tags is not None 
 
 @pytest.mark.asyncio
 async def test_async_payload_integrity_characters_page():
-    chars = await NHentaiAsync().get_characters()
+    chars = await NHentaiAsync().get_characters(1)
     assert chars.page is not None and isinstance(chars.page, int)
     assert chars.total_pages is not None and isinstance(chars.total_pages, int)
     assert chars.characters is not None and isinstance(chars.characters, list)
diff --git a/tests/test_random.py b/tests/test_random.py
index b7575fa..2ea31fe 100644
--- a/tests/test_random.py
+++ b/tests/test_random.py
@@ -5,14 +5,13 @@
 
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
-from NHentai.nhentai import NHentai
-from NHentai.nhentai_async import NHentaiAsync
+from NHentai import NHentai
+from NHentai import NHentaiAsync
 
 def test_standard_case01():
     doujin = NHentai().get_random()
     assert doujin.id is not None
     assert doujin.title is not None
-    assert doujin.secondary_title is not None
     assert doujin.tags is not None
     assert doujin.artists is not None
     assert doujin.languages is not None
@@ -49,7 +48,6 @@ async def test_async_case01():
     doujin = await NHentaiAsync().get_random()
     assert doujin.id is not None
     assert doujin.title is not None
-    assert doujin.secondary_title is not None
     assert doujin.tags is not None
     assert doujin.artists is not None
     assert doujin.languages is not None
diff --git a/tests/test_search.py b/tests/test_search.py
index 3ba2428..e541dfb 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -5,68 +5,185 @@
 
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
-from NHentai.nhentai import NHentai
-from NHentai.nhentai_async import NHentaiAsync
+from NHentai import NHentai
+from NHentai import NHentaiAsync
+from NHentai.entities.doujin import Title
+from NHentai.entities.options import Sort
+
+def test_standard_normal_query_00():
+    search = NHentai().search('naruto')
 
-def test_standard_case01():
-    search = NHentai().search('320165')
     assert search is not None
+    assert search.query is not None
+    assert search.doujins is not None
+    assert search.total_pages is not None
+    assert search.sort is None
+    assert isinstance(search.doujins, list)
+    assert isinstance(search.total_pages, int)
+    assert isinstance(search.query, str)
+
+def test_standard_normal_query_01():
+    search = NHentai().search('full color')
 
-def test_standard_case02():
+    assert search is not None
+    assert search.query is not None
+    assert search.doujins is not None
+    assert search.total_pages is not None
+    assert search.sort is None
+    assert isinstance(search.doujins, list)
+    assert isinstance(search.total_pages, int)
+    assert isinstance(search.query, str)
+
+def test_standard_normal_sort_month():
+    month = NHentai().search(query='full color', sort=Sort.MONTH)
+
+    assert month is not None
+    assert month.query is not None
+    assert month.doujins is not None
+    assert month.total_pages is not None
+    assert month.sort is not None
+    assert isinstance(month.doujins, list)
+    assert isinstance(month.sort, str)
+    assert isinstance(month.total_pages, int)
+    assert isinstance(month.query, str)
+
+def test_standard_normal_sort_week():
+    week = NHentai().search(query='full color', sort=Sort.WEEK)
+
+    assert week is not None
+    assert week.query is not None
+    assert week.doujins is not None
+    assert week.total_pages is not None
+    assert week.sort is not None
+    assert isinstance(week.doujins, list)
+    assert isinstance(week.sort, str)
+    assert isinstance(week.total_pages, int)
+    assert isinstance(week.query, str)
+    
+def test_standard_normal_sort_today():
+    today = NHentai().search(query='full color', sort=Sort.TODAY)
+
+    assert today is not None
+    assert today.query is not None
+    assert today.doujins is not None
+    assert today.total_pages is not None
+    assert today.sort is not None
+    assert isinstance(today.doujins, list)
+    assert isinstance(today.sort, str)
+    assert isinstance(today.total_pages, int)
+    assert isinstance(today.query, str)
+
+def test_standard_normal_sort_all_time():
+    all_time = NHentai().search(query='full color', sort=Sort.ALL_TIME)
+
+    assert all_time is not None
+    assert all_time.query is not None
+    assert all_time.doujins is not None
+    assert all_time.total_pages is not None
+    assert all_time.sort is not None
+    assert isinstance(all_time.doujins, list)
+    assert isinstance(all_time.sort, str)
+    assert isinstance(all_time.total_pages, int)
+    assert isinstance(all_time.query, str)
+
+def test_standard_no_findings_00():
     search = NHentai().search('658468468')
+
     assert len(search.doujins) == 0
+    assert search.total_results == 0
+    assert search.total_pages == 0
 
-def test_standard_case03():
-    search = NHentai().search('naruto')
-    assert len(search.doujins) > 0
+def test_standard_no_findings_01():
+    search = NHentai().search('auhdasudhudasd')
 
-def test_standard_case04():
+    assert len(search.doujins) == 0
+    assert search.total_results == 0
+    assert search.total_pages == 0
+
+def test_standard_direct_id_00():
     search = NHentai().search('320253')
-    assert search is not None
 
-def test_standard_case05():
+    assert search.artists is not None
+    assert search.categories is not None
+    assert search.groups is not None
+    assert search.id is not None
+    assert search.images is not None
+    assert search.languages is not None
+    assert search.parodies is not None
+    assert search.title is not None
+    assert search.total_pages is not None
+    assert isinstance(search.title, Title)
+
+def test_standard_direct_id_01():
     search = NHentai().search('320066')
-    assert search is not None
 
-def test_standard_case06():
-    search = NHentai().search('auhdasudhudasd')
-    assert len(search.doujins) == 0
-
-def test_standard_case07():
-    search = NHentai().search('full color')
-    assert len(search.doujins) > 0
+    assert search.artists is not None
+    assert search.categories is not None
+    assert search.groups is not None
+    assert search.id is not None
+    assert search.images is not None
+    assert search.languages is not None
+    assert search.parodies is not None
+    assert search.title is not None
+    assert search.total_pages is not None
+    assert isinstance(search.title, Title)
 
 @pytest.mark.asyncio
-async def test_async_case01():
-    search = await NHentaiAsync().search('320165')
+async def test_async_instance_check():
+    search = await NHentaiAsync().search('naruto')
+    
     assert search is not None
+    assert search.query is not None
+    assert search.doujins is not None
+    assert search.total_pages is not None
+    assert search.sort is None
+    assert isinstance(search.doujins, list)
+    assert isinstance(search.total_pages, int)
+    assert isinstance(search.query, str)
 
 @pytest.mark.asyncio
-async def test_async_case02():
+async def test_async_no_findings_00():
     search = await NHentaiAsync().search('658468468')
+
     assert len(search.doujins) == 0
+    assert search.total_results == 0
+    assert search.total_pages == 0
 
 @pytest.mark.asyncio
-async def test_async_case03():
-    search = await NHentaiAsync().search('naruto')
-    assert len(search.doujins) > 0
+async def test_async_no_findings_01():
+    search = await NHentaiAsync().search('21312322231')
+
+    assert len(search.doujins) == 0
+    assert search.total_results == 0
+    assert search.total_pages == 0
 
 @pytest.mark.asyncio
-async def test_async_case04():
+async def test_async_direct_id_00():
     search = await NHentaiAsync().search('320253')
-    assert search is not None
+    
+    assert search.artists is not None
+    assert search.categories is not None
+    assert search.groups is not None
+    assert search.id is not None
+    assert search.images is not None
+    assert search.languages is not None
+    assert search.parodies is not None
+    assert search.title is not None
+    assert search.total_pages is not None
+    assert isinstance(search.title, Title)
 
 @pytest.mark.asyncio
-async def test_async_case05():
+async def test_async_direct_id_01():
     search = await NHentaiAsync().search('320066')
-    assert search is not None
-
-@pytest.mark.asyncio
-async def test_async_case06():
-    search = await NHentaiAsync().search('auhdasudhudasd')
-    assert len(search.doujins) == 0
 
-@pytest.mark.asyncio
-async def test_async_case07():
-    search = await NHentaiAsync().search('full color')
-    assert len(search.doujins) > 0
+    assert search is not None
+    assert search.artists is not None
+    assert search.categories is not None
+    assert search.groups is not None
+    assert search.id is not None
+    assert search.images is not None
+    assert search.languages is not None
+    assert search.parodies is not None
+    assert search.title is not None
+    assert search.total_pages is not None
+    assert isinstance(search.title, Title)