From 5cb3bccf4577fe00b7eed25ca45145a9ec64c3b6 Mon Sep 17 00:00:00 2001 From: praschke Date: Sat, 17 Feb 2024 23:59:36 +0000 Subject: [PATCH] Add support for syosetu.com --- calibre-plugin/plugin-defaults.ini | 127 ++++++ fanficfare/adapters/__init__.py | 1 + fanficfare/adapters/adapter_syosetucom.py | 466 ++++++++++++++++++++++ fanficfare/configurable.py | 6 +- fanficfare/defaults.ini | 127 ++++++ 5 files changed, 725 insertions(+), 2 deletions(-) create mode 100644 fanficfare/adapters/adapter_syosetucom.py diff --git a/calibre-plugin/plugin-defaults.ini b/calibre-plugin/plugin-defaults.ini index 93c04822..905860d9 100644 --- a/calibre-plugin/plugin-defaults.ini +++ b/calibre-plugin/plugin-defaults.ini @@ -2699,6 +2699,133 @@ cover_exclusion_regexp:/css/bir.png ## for those not expecting it. #append_datepublished_to_storyurl:false +[syosetu.com] +use_basic_cache:true + +## Clear FanFiction from defaults, site is original fiction. +extratags: + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:youremail@yourdomain.dom +#password:yourpassword + +## In order to get bookmark info, you need to login all the time. +## This defaults to off to save time and network traffic. +## Requires valid syosetu user email/numeric ID and password when true. +#always_login:false + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + +## Some adapters collect additional meta information beyond the +## standard ones. They need to be defined in extra_valid_entries to +## tell the rest of the FanFicFare system about them. They can be +## used in include_subject_tags, titlepage_entries, +## extra_titlepage_entries, logpage_entries, extra_logpage_entries, +## and include_in_* config items. You can also add additional entries +## here to build up composite metadata entries. + +extra_valid_entries: fullgenre, biggenre, smallgenre, + imprint, warningtags, freeformtags, comments, reviews, + bookmarks, ratingpoints, overallpoints, bookmarked, + bookmarkcategory, bookmarkmemo, bookmarkprivate, subscribed +## Genres are only present on general stories. +## fullgenre looks like smallgenre〔biggenre〕 +## e.g. High Fantasy (Fantasy) or Other World (Romance) +## The full list is at https://dev.syosetu.com/man/api/ +## under the section titled 'ジャンル指定' +fullgenre_label:ジャンル +biggenre_label:大ジャンル +smallgenre_label:小ジャンル +## Imprint is only present on adult stories +## ノクターンノベルズ - male demographic +## ムーンライトノベルズ - female demographic +## ミッドナイトノベルズ - other +imprint_label:掲載サイト +## Warnings are required flags but are displayed as tags +## R15 - some adult stories have this anyway +## ボーイズラブ - boy's love +## ガールズラブ - girl's love +## 残酷な描写あり - graphic depictions (of violence, bullying, etc) +## 異世界転生 - reincarnation in another world +## 異世界転移 - transmigration to another world +warningtags_label:必須キーワード +freeformtags_label:キーワード +comments_label:感想 +reviews_label:レビュー +## Count of bookmarks on story by all users +bookmarks_label:ブックマーク登録 +## 2 points per user star +ratingpoints_label:評価ポイント +## Bookmarks become an extra 2 points +overallpoints_label:総合評価 +## Information from *your* bookmark on the story. Only collected +## if always_login:true +bookmarked_label:ブックマーク +bookmarkcategory_label:ブックマークカテゴリ +bookmarkmemo_label:ブックマークメモ +bookmarkprivate_label:非公開ブックマーク +subscribed_label:更新通知 + +include_in_warnings: warningtags + +include_in_genre: fullgenre +#include_in_genre: biggenre, smallgenre + +## adds to titlepage_entries instead of replacing it. +#extra_titlepage_entries: freeformtags,comments,reviews,bookmarks,ratingpoints,overallpoints,bookmarked,bookmarkmemo + +## adds to include_subject_tags instead of replacing it. +#extra_subject_tags: warningtags,freeformtags + +## syosetu.com allows authors to group chapters (episodes) by titled sections. +## true prepends the section title to every episode title. +## firstepisode prepends the section title to the first episode title only. (default) +## false means section titles are not present in the end result. +#prepend_section_titles:firstepisode + +## syosetu.com chapters can have author notes attached to them. +## Setting include_author_notes:false will exclude them with the +## chapter text. +#include_author_notes:true + +add_to_output_css: + .novel_p, .novel_a { border: 1px solid; border-collapse: collapse; padding: 0.5em; margin: 0.5em; } + +## suggested Japanese labels for default metadata +#title_label:作品タイトル +#storyUrl_label:作品URL +#description_label:あらすじ +#author_label:作者名 +#authorUrl_label:作者URL +#formatname_label:ファイル形式 +#formatext_label:拡張子 +#category_label:カテゴリ +#genre_label:ジャンル +#language_label:言語 +#series_label:シリーズ +#seriesUrl_label:シリーズURL +#seriesHTML_label:シリーズ +#status_label:状態 +#datePublished_label:掲載日 +#dateUpdated_label:最終更新日 +#dateCreated_label:作成日 +#rating_label:レイティング +#warnings_label:警告 +#numChapters_label:部分数 +#numWords_label:文字数 +#site_label:掲載サイト +#publisher_label:掲載サイト +#siteabbrev_label:サイト略称 +#storyId_label:作品ID +#authorId_label:作者ID +#extratags_label:追加のタグ + [t.evancurrie.ca] # was fanfiction.tenhawkpresents.ink use_basic_cache:true diff --git a/fanficfare/adapters/__init__.py b/fanficfare/adapters/__init__.py index d47e031d..46699918 100644 --- a/fanficfare/adapters/__init__.py +++ b/fanficfare/adapters/__init__.py @@ -136,6 +136,7 @@ from . import adapter_psychficcom from . import adapter_deviantartcom from . import adapter_readonlymindcom from . import adapter_wwwsunnydaleafterdarkcom +from . import adapter_syosetucom ## This bit of complexity allows adapters to be added by just adding ## importing. It eliminates the long if/else clauses we used to need diff --git a/fanficfare/adapters/adapter_syosetucom.py b/fanficfare/adapters/adapter_syosetucom.py new file mode 100644 index 00000000..a8528bb4 --- /dev/null +++ b/fanficfare/adapters/adapter_syosetucom.py @@ -0,0 +1,466 @@ +# -*- coding: utf-8 -*- + +# Copyright 2013 Fanficdownloader team, 2018 FanFicFare team +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import absolute_import +import logging, time +logger = logging.getLogger(__name__) +import os, re, math + +from hashlib import sha256 +from base64 import urlsafe_b64encode as b64encode +from datetime import timezone, timedelta + +from bs4.element import Comment +from .. import exceptions as exceptions + +# py2 vs py3 transition +from ..six.moves import http_cookiejar as cl +from ..six.moves.urllib.parse import urlparse +from ..six import text_type as unicode + +from .base_adapter import BaseSiteAdapter, makeDate + +tzJST = timezone(timedelta(hours=9), name='JST') + +def getClass(): + return SyosetuComAdapter + +def getEntry(soup, *args): + for arg in args: + target = soup.find('th', string=arg) + if target is not None: + return target.findNext('td') + return None + +class SyosetuComAdapter(BaseSiteAdapter): + + def __init__(self, config, url): + BaseSiteAdapter.__init__(self, config, url) + + self.story.setMetadata('siteabbrev', 'syosetu') + self.story.setMetadata('language', 'Japanese') + + splitPath = self.path.split('/') + self.storyId = splitPath[-2] if (splitPath[-1] == '') else splitPath[-1] + self.story.setMetadata('storyId', self.storyId) + + self._setURL('https://' + self.host + '/' + self.storyId + '/') + + self.is_adult = False + + @staticmethod + def getSiteDomain(): + return 'syosetu.com' + + @classmethod + def getAcceptDomains(cls): + return [ + 'ncode.syosetu.com', + 'novel18.syosetu.com', + 'mypage.syosetu.com', + 'xmypage.syosetu.com', + ] + + @classmethod + def getSiteExampleURLs(cls): + return ("https://ncode.syosetu.com/n1234ab/ " + +"https://novel18.syosetu.com/n1234a " + +"https://ncode.syosetu.com/novelview/infotop/ncode/n1234ab " + +"https://novel18.syosetu.com/novelview/infotop/ncode/n1234a/") + + def getSiteURLPattern(self): + return r"^https?://(ncode|novel18)\.syosetu\.com/(novelview/infotop/ncode/)?n[0-9]+[a-z]+/?$" + + def set_adult_cookie(self): + cookie = cl.Cookie(version=0, name='over18', value='yes', + port=None, port_specified=False, + domain=self.getSiteDomain(), domain_specified=False, domain_initial_dot=False, + path='/', path_specified=True, + secure=False, + expires=time.time()+10000, + discard=False, + comment=None, + comment_url=None, + rest={'HttpOnly': None}, + rfc2109=False) + self.get_configuration().get_cookiejar().set_cookie(cookie) + + def performLogin(self, url): + params = {} + if self.password: + params['narouid'] = self.username + params['pass'] = self.password + else: + params['narouid'] = self.getConfig('username') + params['pass'] = self.getConfig('password') + + if params['narouid'] and params['pass']: + loginUrl = 'https://syosetu.com/login/login/' + logger.info("Will now login to URL (%s) as (%s)" % (loginUrl, + params['narouid'])) + d = self.post_request(loginUrl, params) + if 'href="https://syosetu.com/login/logout/"' not in d: + logger.info("Failed to login to URL %s as %s" % (loginUrl, + params['narouid'])) + raise exceptions.FailedToLogin(url,params['username']) + + def extractChapterUrlsAndMetadata(self): + """ + Oneshots are located at /n1234ab/ + + Serials are located at /n1234ab/#/ (# is a non-padded number, + like 1, 2, ..., 394). Serials can have a single chapter. + + Most metadata is located at /novelview/infotop/ncode/n1234ab/ + + Chapter publish and update times are located at /n1234ab/?p=# + paginated in groups of 100 + """ + + if self.is_adult or self.getConfig('is_adult'): + self.set_adult_cookie() + + # self.performLogin(self.url) + + infoUrl = 'https://' + self.host + '/novelview/infotop/ncode/' + self.storyId + '/' + # don't use cache if manual is_adult--should only happen + # if it's an adult story and they don't have is_adult in ini. + (infoData,infoRurl) = self.get_request_redirected(infoUrl, + usecache=(not self.is_adult)) + # IDs for general (adult) stories redirect to ncode (novel18) + # despite IDs being shared, stories can't be age-restricted automatically + if infoUrl != infoRurl: + infoUrl = infoRurl + self.host = urlparse(infoRurl).netloc + self._setURL('https://' + self.host + '/' + self.storyId + '/') + + if (self.host.split('.')[0] == 'novel18'): + if not (self.is_adult or self.getConfig("is_adult")): + raise exceptions.AdultCheckRequired(self.url) + + # Did not find story. (Invalid ID) + if '投稿済作品が見つかりません。' in infoData: + raise exceptions.StoryDoesNotExist(self.url) + + # Story has been deleted. + if 'この作品は作者によって削除されました。' in infoData: + raise exceptions.StoryDoesNotExist(self.url) + + if self.getConfig('always_login') and 'href="https://syosetu.com/login/input/"' in infoData: + self.performLogin(self.url) + infoData = self.get_request(infoUrl, usecache=False) + + infoSoup = self.make_soup(infoData) + + # Title + + title = infoSoup.find('a', href=self.url).text.strip() + self.story.setMetadata('title', title) + + # Author + + # the author URL can always be found at the bottom of the page + # differs between ncode and novel18 + authorUrl = (infoSoup.find('a', string='作者マイページ') + or infoSoup.find('a', string='作者Xマイページ'))['href'] + self.story.setMetadata('authorUrl', authorUrl) + + authorId = urlparse(authorUrl).path.split('/')[1] + self.story.setMetadata('authorId', authorId) + + authorElement = getEntry(infoSoup, '作者名') + author = authorElement.text.strip() + try: + if authorElement.find('a') is None: + # when the author isn't linked in the table, a pseudonym has been used + realAuthor = self.make_soup(self.get_request(authorUrl)).find('title').text.strip() + if realAuthor != author: + author = author + ' (' + realAuthor + ')' + except: + logger.info('Author parsing failed, using pseudonym.') + self.story.setMetadata('author', author) + + # Description + + description = getEntry(infoSoup, 'あらすじ') + description.name = 'div' + description['class'] = 'description' + self.setDescription(self.url, description) + + # Date Published and Updated + + # 2017年 05月16日 17時30分 + published = makeDate(getEntry(infoSoup, '掲載日').text.strip(), + '%Y年 %m月%d日 %H時%M分').replace(tzinfo=tzJST) + self.story.setMetadata('datePublished', published) + + updated = published + updateElement = getEntry(infoSoup, + '最終部分掲載日', # last part published (complete) + '最新部分掲載日', # latest part published + '最終更新日' # last update + ) + if updateElement is not None: + updated = makeDate(updateElement.text.strip(), + '%Y年 %m月%d日 %H時%M分').replace(tzinfo=tzJST) + self.story.setMetadata('dateUpdated', updated) + + # Series + + # differs between ncode and novel18 + series = getEntry(infoSoup, 'シリーズ', 'Xシリーズ') + try: + if series is not None: + seriesName = series.text.strip() + seriesUrl = series.find('a')['href'] + + seriesSoup = self.make_soup(self.get_request(seriesUrl)) + alist = seriesSoup.select('.serieslist .title a') + i = 1 + for a in alist: + if a['href'] == '/' + self.storyId + '/': + self.setSeries(seriesName, i) + self.story.setMetadata('seriesUrl', seriesUrl) + break + i += 1 + except: + logger.info('Series parsing failed.') + + # Character count + + # 123,789文字 + numMoji = int(getEntry(infoSoup, '文字数').text.strip().replace(',', '')[:-2]) + self.story.setMetadata('numWords', numMoji) + + # Status and Chapter count + + noveltype = (infoSoup.find(id='noveltype') + or infoSoup.find(id='noveltype_notend')) + if noveltype.text.strip() == '短編': + numChapters = 1 + oneshot = True + completed = True + else: + # '全1,292部分\n' + numChapters = int(noveltype.next_sibling.strip().replace(',', '')[1:-2]) + oneshot = False + completed = True if noveltype == '完結済' else False + self.story.setMetadata('numChapters', numChapters) + self.story.setMetadata('status', 'Completed' if completed else 'In-Progress') + + # Keywords + + flags = [] + # not sure what it looks like if a work has no tags + tagsElement = getEntry(infoSoup, 'キーワード') + if tagsElement.find('span'): + # R15, ボーイズラブ, ガールズラブ, 残酷な描写あり, 異世界転生, 異世界転移 + flags = tagsElement.find('span').text.split() + for flag in flags: + self.story.addToList('warningtags', flag) + for tag in tagsElement.contents[-1].text.split(): + self.story.addToList('freeformtags', tag) + + # Rating, Genre, and Imprint + + if self.host.split('.')[0] == 'novel18': + rating = 'R18' + # ミッドナイトノベルズ(大人向け) + imprint = getEntry(infoSoup, '掲載サイト').text.strip().split('(')[0] + self.story.setMetadata('imprint', imprint) + else: + rating = 'R15' if 'R15' in flags else 'G' + # ハイファンタジー〔ファンタジー〕 + fullgenre = getEntry(infoSoup, 'ジャンル').text.strip() + self.story.setMetadata('fullgenre', fullgenre) + smallgenre = fullgenre.split('〔')[0] + self.story.setMetadata('smallgenre', smallgenre) + biggenre = fullgenre.split('〔')[1][:-1] + self.story.setMetadata('biggenre', biggenre) + self.story.setMetadata('rating', rating) + + # Comments, Reviews, Bookmarks, Points + + commentsElement = getEntry(infoSoup, '感想') + reviewsElement = getEntry(infoSoup, 'レビュー') + bookmarksElement = getEntry(infoSoup, 'ブックマーク登録') + ratingPointsElement = getEntry(infoSoup, '総合評価') + overallPointsElement = getEntry(infoSoup, '評価ポイント') + + # if the story is unlinked from author page, stats will be hidden + + # '\n116件\n\n' + if commentsElement is not None: + self.story.setMetadata('comments', + int(commentsElement.next_element.strip().replace(',', '')[:-1])) + + # 171件 + if reviewsElement is not None: + self.story.setMetadata('reviews', + int(reviewsElement.next_element.strip().replace(',', '')[:-1])) + + # 108,610件 + if bookmarksElement is not None: + self.story.setMetadata('bookmarks', + int(bookmarksElement.next_element.strip().replace(',', '')[:-1])) + + # 166,944pt or ※非公開 + if (ratingPointsElement is not None and + ratingPointsElement.text.strip() != '※非公開'): + self.story.setMetadata('ratingpoints', + int(ratingPointsElement.next_element.strip().replace(',', '')[:-2])) + + # 384,164pt or ※非公開 + if (overallPointsElement is not None and + overallPointsElement.text.strip() != '※非公開'): + self.story.setMetadata('overallpoints', + int(overallPointsElement.next_element.strip().replace(',', '')[:-2])) + + # Bookmark metadata + + if self.getConfig("always_login"): + if infoSoup.find('div', {'data-remodal-id':'setting_bookmark'}) is None: + self.story.setMetadata('bookmarked', False) + else: + self.story.setMetadata('bookmarked', True) + modal = infoSoup.find('div', {'class':'favnovelmain_update'}) + + # bookmark category name + bookmarkCategory = modal.find('option', { + 'class':'js-category_select', + 'selected':'selected'}).text.strip() + self.story.setMetadata('bookmarkcategory', bookmarkCategory) + + #bookmarkmemo + if modal.find('input', {'class':'js-bookmark_memo'}).has_attr('value'): + self.story.setMetadata('bookmarkmemo', + modal.find('input', {'class':'js-bookmark_memo'})['value'].strip()) + + #bookmarkprivate + self.story.setMetadata('bookmarkprivate', + modal.find('input', { + 'class':'bookmark_jyokyo', + 'value':'1'}).has_attr('checked')) + + #subscribed + self.story.setMetadata('subscribed', + modal.find('input', {'name':'isnotice'}).has_attr('checked')) + + if oneshot: + self.add_chapter(title, self.url) + logger.debug("Story: <%s>", self.story) + return + + # serialized story + + prependSectionTitles = self.getConfig('prepend_section_titles', 'firstepisode') + + tocSoups = [] + for n in range(1, math.ceil(numChapters/100)+1): + tocPage = self.make_soup(self.get_request(self.url + '?p=%s' % n)) + tocSoups.append(tocPage.find('div',{'class':'index_box'})) + + sectionTitle = None + newSection = False + for tocSoup in tocSoups: + for child in tocSoup.findChildren(recursive=False): + if 'chapter_title' in child['class']: + sectionTitle = child.text.strip() + newSection = True + elif 'novel_sublist2' in child['class']: + epTitle = child.find('a').text.strip() + updateElement = child.find('dt', {'class':'long_update'}) + epPublished = updateElement.next_element.strip() + epUpdated = '' + if updateElement.find('span') is not None: + epUpdated = updateElement.find('span')['title'].strip() + uniqueKey = b64encode(sha256(('title ' + epTitle + + ' published ' + epPublished + + ' updated ' + epUpdated).encode()).digest()).decode() + epUrl = 'https://' + self.host + child.find('a')['href'] + '#' + uniqueKey + + if ((sectionTitle is not None) and + ((newSection and prependSectionTitles == 'firstepisode') or + prependSectionTitles == 'true')): + # bracket with ZWSP to mark presence of the section title + epTitle = '\u200b' + sectionTitle + '\u3000\u200b' + epTitle + + self.add_chapter(epTitle, epUrl) + newSection = False + + logger.debug("Story: <%s>", self.story) + return + + def getChapterText(self, url): + logger.debug('Getting chapter text from <%s>' % url) + + soup = self.make_soup(self.get_request(url)) + + if self.getConfig('include_author_notes', True): + divs = soup.find_all('div', id=re.compile(r'^novel_(p|honbun|a)$')) + if divs is None: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + text_divs = [] + for div in divs: + div['class'].append(div['id']) + div.attrs.pop('id') + text_divs.append(unicode(div)) + soup = self.make_soup(' '.join(text_divs)) + else: + soup = soup.find('div', id='novel_honbun') + soup['class'].append(soup['id']) + soup.attrs.pop('id') + if soup is None: + raise exceptions.FailedToDownload("Error downloading Chapter: %s! Missing required element!" % url) + + return self.utf8FromSoup(url, soup) + + def before_get_urls_from_page(self,url,normalize): + # syosetu doesn't show adult series or author pages without the cookie + if self.getConfig("is_adult"): + self.set_adult_cookie() + + def get_urls_from_page(self,url,normalize): + from ..geturls import get_urls_from_html + # Supporting story page and info page URLs means both links get picked up + # and return duplicate story IDs without a custom handler. + + # hook for logins, etc. + self.before_get_urls_from_page(url,normalize) + + # this way it uses User-Agent or other special settings. + data = self.get_request(url,usecache=False) + parsedUrlList = get_urls_from_html(self.make_soup(data), + url, + configuration=self.configuration, + normalize=normalize) + + urlList = [] + ncodes = [] + for storyUrl in parsedUrlList: + parsedUrl = urlparse(storyUrl) + host = parsedUrl.netloc + if host in ['ncode.syosetu.com', 'novel18.syosetu.com']: + splitPath = parsedUrl.path.split('/') + storyId = splitPath[-2] if (splitPath[-1] == '') else splitPath[-1] + if storyId not in ncodes: + ncodes.append(storyId) + urlList.append('https://' + host + '/' + storyId + '/') + else: + urlList.append(storyUrl) + + return {'urllist':urlList} diff --git a/fanficfare/configurable.py b/fanficfare/configurable.py index 6a005fa0..dadf0e81 100644 --- a/fanficfare/configurable.py +++ b/fanficfare/configurable.py @@ -234,9 +234,9 @@ def get_valid_set_options(): 'fix_fimf_blockquotes':(['fimfiction.net'],None,boollist), 'fail_on_password':(['fimfiction.net'],None,boollist), 'keep_prequel_in_description':(['fimfiction.net'],None,boollist), - 'include_author_notes':(['fimfiction.net','readonlymind.com','royalroad.com'],None,boollist), + 'include_author_notes':(['fimfiction.net','readonlymind.com','royalroad.com','syosetu.com'],None,boollist), 'do_update_hook':(['fimfiction.net']+otw_list,None,boollist), - 'always_login':(otw_list+base_xenforo_list,None,boollist), + 'always_login':(['syosetu.com']+otw_list+base_xenforo_list,None,boollist), 'use_archived_author':(otw_list,None,boollist), 'use_view_full_work':(otw_list+['fanfics.me'],None,boollist), 'use_workskin':(otw_list,None,boollist), @@ -312,6 +312,7 @@ def get_valid_set_options(): 'dedup_order_chapter_list': (['wuxiaworld.xyz', 'novelupdates.cc'], None, boollist), 'show_nsfw_cover_images': (['fiction.live'], None, boollist), 'show_timestamps': (['fiction.live'], None, boollist), + 'prepend_section_titles': (['syosetu.com'], None, boollist+['firstepisode']), } return dict(valdict) @@ -582,6 +583,7 @@ def get_valid_keywords(): 'show_spoiler_tags', 'max_zalgo', 'epub_version', + 'prepend_section_titles', ]) # *known* entry keywords -- or rather regexps for them. diff --git a/fanficfare/defaults.ini b/fanficfare/defaults.ini index 854c2509..5d89e0fc 100644 --- a/fanficfare/defaults.ini +++ b/fanficfare/defaults.ini @@ -2694,6 +2694,133 @@ cover_exclusion_regexp:/css/bir.png ## for those not expecting it. #append_datepublished_to_storyurl:false +[syosetu.com] +use_basic_cache:true + +## Clear FanFiction from defaults, site is original fiction. +extratags: + +## Some sites require login (or login for some rated stories) The +## program can prompt you, or you can save it in config. In +## commandline version, this should go in your personal.ini, not +## defaults.ini. +#username:youremail@yourdomain.dom +#password:yourpassword + +## In order to get bookmark info, you need to login all the time. +## This defaults to off to save time and network traffic. +## Requires valid syosetu user email/numeric ID and password when true. +#always_login:false + +## Some sites also require the user to confirm they are adult for +## adult content. In commandline version, this should go in your +## personal.ini, not defaults.ini. +#is_adult:true + +## Some adapters collect additional meta information beyond the +## standard ones. They need to be defined in extra_valid_entries to +## tell the rest of the FanFicFare system about them. They can be +## used in include_subject_tags, titlepage_entries, +## extra_titlepage_entries, logpage_entries, extra_logpage_entries, +## and include_in_* config items. You can also add additional entries +## here to build up composite metadata entries. + +extra_valid_entries: fullgenre, biggenre, smallgenre, + imprint, warningtags, freeformtags, comments, reviews, + bookmarks, ratingpoints, overallpoints, bookmarked, + bookmarkcategory, bookmarkmemo, bookmarkprivate, subscribed +## Genres are only present on general stories. +## fullgenre looks like smallgenre〔biggenre〕 +## e.g. High Fantasy (Fantasy) or Other World (Romance) +## The full list is at https://dev.syosetu.com/man/api/ +## under the section titled 'ジャンル指定' +fullgenre_label:ジャンル +biggenre_label:大ジャンル +smallgenre_label:小ジャンル +## Imprint is only present on adult stories +## ノクターンノベルズ - male demographic +## ムーンライトノベルズ - female demographic +## ミッドナイトノベルズ - other +imprint_label:掲載サイト +## Warnings are required flags but are displayed as tags +## R15 - some adult stories have this anyway +## ボーイズラブ - boy's love +## ガールズラブ - girl's love +## 残酷な描写あり - graphic depictions (of violence, bullying, etc) +## 異世界転生 - reincarnation in another world +## 異世界転移 - transmigration to another world +warningtags_label:必須キーワード +freeformtags_label:キーワード +comments_label:感想 +reviews_label:レビュー +## Count of bookmarks on story by all users +bookmarks_label:ブックマーク登録 +## 2 points per user star +ratingpoints_label:評価ポイント +## Bookmarks become an extra 2 points +overallpoints_label:総合評価 +## Information from *your* bookmark on the story. Only collected +## if always_login:true +bookmarked_label:ブックマーク +bookmarkcategory_label:ブックマークカテゴリ +bookmarkmemo_label:ブックマークメモ +bookmarkprivate_label:非公開ブックマーク +subscribed_label:更新通知 + +include_in_warnings: warningtags + +include_in_genre: fullgenre +#include_in_genre: biggenre, smallgenre + +## adds to titlepage_entries instead of replacing it. +#extra_titlepage_entries: freeformtags,comments,reviews,bookmarks,ratingpoints,overallpoints,bookmarked,bookmarkmemo + +## adds to include_subject_tags instead of replacing it. +#extra_subject_tags: warningtags,freeformtags + +## syosetu.com allows authors to group chapters (episodes) by titled sections. +## true prepends the section title to every episode title. +## firstepisode prepends the section title to the first episode title only. (default) +## false means section titles are not present in the end result. +#prepend_section_titles:firstepisode + +## syosetu.com chapters can have author notes attached to them. +## Setting include_author_notes:false will exclude them with the +## chapter text. +#include_author_notes:true + +add_to_output_css: + .novel_p, .novel_a { border: 1px solid; border-collapse: collapse; padding: 0.5em; margin: 0.5em; } + +## suggested Japanese labels for default metadata +#title_label:作品タイトル +#storyUrl_label:作品URL +#description_label:あらすじ +#author_label:作者名 +#authorUrl_label:作者URL +#formatname_label:ファイル形式 +#formatext_label:拡張子 +#category_label:カテゴリ +#genre_label:ジャンル +#language_label:言語 +#series_label:シリーズ +#seriesUrl_label:シリーズURL +#seriesHTML_label:シリーズ +#status_label:状態 +#datePublished_label:掲載日 +#dateUpdated_label:最終更新日 +#dateCreated_label:作成日 +#rating_label:レイティング +#warnings_label:警告 +#numChapters_label:部分数 +#numWords_label:文字数 +#site_label:掲載サイト +#publisher_label:掲載サイト +#siteabbrev_label:サイト略称 +#storyId_label:作品ID +#authorId_label:作者ID +#extratags_label:追加のタグ + [t.evancurrie.ca] # was fanfiction.tenhawkpresents.ink use_basic_cache:true