Jav小司机

Jav小司机。简单轻量速度快!

As of 01.06.2019. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Jav小司机
// @namespace    wddd
// @version      1.0.0
// @author       wddd
// @license      MIT
// @include      http*://*javlibrary.com/*
// @include      http*://*javlib.com/*
// @description  Jav小司机。简单轻量速度快!
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @homepage     https://github.com/wdwind/JavMiniDriver
// ==/UserScript==

// Credit to
//  * https://greasyfork.org/zh-CN/scripts/25781-jav%E8%80%81%E5%8F%B8%E6%9C%BA
//  * https://greasyfork.org/zh-CN/scripts/37122-javlibrary-preview

// Utils

function setCookie(cookieName, cookieValue, expireDays) {
    let expireDate =new Date();
    expireDate.setDate(expireDate.getDate() + expireDays);
    let expires = "expires=" + ((expireDays == null) ? '' : expireDate.toUTCString());
    document.cookie = cookieName + "=" + cookieValue + ";" + expires + ";path=/";
}

function insertAfter(newNode, referenceNode) {
    referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}

function parseHTMLText(text) {
    try {
        let doc = document.implementation.createHTMLDocument('');
        doc.documentElement.innerHTML = text;
        return doc;
    } catch (e) {
        console.error('Parse error');
    }
}

// https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro
function createElementFromHTML(html) {
    var template = document.createElement('template');
    html = html.trim(); // Never return a text node of whitespace as the result
    template.innerHTML = html;
    return template.content.firstChild;
}

function getLoadMoreButton(buttonId, callback) {
    let loadMoreButton = createElementFromHTML('<input type="button" class="largebutton" value="加载更多 &or;">');
    loadMoreButton.addEventListener('click', callback);

    buttonId = (buttonId != null) ? buttonId : 'load_more';
    let loadMoreDiv = createElementFromHTML(`<div id="${buttonId}" class="load_more"></div>`);
    loadMoreDiv.appendChild(loadMoreButton);
    return loadMoreDiv;
}

// For the requests in different domains
// GM_xmlhttpRequest is made from the background page, and as a result, it 
// doesn't have cookies in the request header
function gmFetch(obj) {
    return new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: obj.method || 'GET',
            // timeout in ms
            timeout: obj.timeout,
            url: obj.url,
            headers: obj.headers,
            data: obj.data,
            onload: (result) => {
                if (result.status >= 200 && result.status < 300) {
                    resolve(result);
                } else {
                    reject(result);
                }
            },
            onerror: reject,
            ontimeout: reject,
        });
    });
}

// For the requests in the same domain
// XMLHttpRequest is made within the page, so it will send the cookies
function xhrFetch(obj) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest();
        xhr.open(obj.method || 'GET', obj.url);
        // timeout in ms
        xhr.timeout = obj.timeout;
        if (obj.headers) {
            Object.keys(obj.headers).forEach(key => {
                xhr.setRequestHeader(key, obj.headers[key]);
            });
        }
        xhr.withCredentials = true;
        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(xhr);
            } else {
                reject(xhr);
            }
        };
        xhr.onerror = () => reject(xhr);
        xhr.ontimeout = () => reject(xhr);
        xhr.send(obj.data);
    });
};

// Style

function addStyle() {
    // social media
    GM_addStyle(`
        #toplogo {
            height: 55px;
        }
        .socialmedia {
            display: none !important;
            width: 0% !important;
            height: 0% !important;
        }
        .videothumblist .videos .video {
            height: 290px;
        }
    `);

    // left menu
    if (window.location.href.includes('?v=')) {
        GM_addStyle(`
            #leftmenu {
                display: none;
                width: 0%;
            }
            #rightcolumn {
                margin-left: 10px;
            }
            /*
            #video_title .post-title:hover {
                text-decoration: underline;
                text-decoration-color: #CCCCCC;
            }
            */
            #video_id .text {
                color: red;
            }
            #torrents > table {
                width:100%;
                text-align: center;
                border: 2px solid grey;
            }
            #torrents tr td + td {
                border-left: 2px solid grey;
            }
            #video_favorite_edit {
                margin-bottom: 20px;
            }
            #torrents {
                margin-bottom: 20px;
            }
            #preview {
                margin-bottom: 20px;
            }
            #preview video {
                max-width: 100%;
            }
            #full_screenshot {
                cursor: pointer;
                width: 15%;
            }
            .clickToCopy {
                cursor: pointer;
            }
            .load_more {
                text-align: center;
            }
        `);
    }

    // Homepage
    if (!window.location.href.includes('.php')) {
        GM_addStyle(`
            .videothumblist {
                height: 645px !important;
            }
        `);
    }
}

// Thumbnail

class MiniDriverThumbnail {

    constructor() {
        this.nextPageSelectorId = 'load_next_page';
    }

    execute() {
        this.updateVideoDetails(document);
        this.updateNextPage();
    }

    updateVideoDetails(doc) {
        let videos = doc.querySelectorAll('.videothumblist .videos .video');
        Array.from(videos).forEach(async video => {
            if (video.id.includes('vid_')) {
                let request = {url: `http://www.javlibrary.com/cn/?v=${video.id.substring(4)}`};
                let result = await xhrFetch(request).catch(err => {console.log(err); return;});
                let videoDetailsDoc = parseHTMLText(result.responseText);

                // Video date
                let videoDate = '';
                if (videoDetailsDoc.getElementById('video_date')) {
                    videoDate = videoDetailsDoc.getElementById('video_date').getElementsByClassName('text')[0].innerText;
                }

                // Video score
                let videoScore = '';
                if (videoDetailsDoc.getElementById('video_review')) {
                    let videoScoreStr = videoDetailsDoc.getElementById('video_review').getElementsByClassName('score')[0].innerText;
                    videoScore = videoScoreStr.substring(1, videoScoreStr.length - 1);
                }

                // Video watched
                let videoWatched = '';
                if (videoDetailsDoc.getElementById('watched')) {
                    videoWatched = videoDetailsDoc.getElementById('watched').getElementsByTagName('a')[0].innerText;
                }
                
                let videoDetailsHtml = `
                    <div>
                        <span>${videoDate}</span>&nbsp;&nbsp;<span style='color:red;'>${videoScore}</span>
                        <br/>
                        <span>${videoWatched} 人看过</span>
                    </div>
                `;
                let videoDetails = createElementFromHTML(videoDetailsHtml);
                video.insertBefore(videoDetails, video.getElementsByClassName('toolbar')[0]);
            }
        });
    }

    async getNextPage(url) {
        // Fetch next page
        let result = await xhrFetch({url: url}).catch(err => {console.log(err); return;});
        let nextPageDoc = parseHTMLText(result.responseText);
        this.updateVideoDetails(nextPageDoc);

        // Update current page
        let videos = document.getElementsByClassName('videos');
        let nextPageVideos = nextPageDoc.getElementsByClassName('videos');
        if (nextPageVideos.length > 0) {
            Array.from(nextPageVideos[0].children).forEach(video => {
                videos[0].appendChild(video);
            });
        }
        
        let pageSelectorDiv = document.getElementsByClassName('page_selector')[0];
        pageSelectorDiv.innerText = '';
        
        // Add next page button
        let nextPage = nextPageDoc.getElementsByClassName('page next');
        if (nextPage.length > 0) {
            let nextPageUrl = nextPage[0].href;
            let nextPageButton = getLoadMoreButton(this.nextPageSelectorId, async () => this.getNextPage(nextPageUrl));
            pageSelectorDiv.appendChild(nextPageButton);
        }
    }

    updateNextPage() {
        let nextPage = document.getElementsByClassName('page next');
        if (nextPage.length > 0) {
            let nextPageUrl = nextPage[0].href;
            let nextPageButton = getLoadMoreButton('load_next_page', async () => this.getNextPage(nextPageUrl));
            
            let pageSelectorDiv = document.getElementsByClassName('page_selector')[0];
            pageSelectorDiv.innerText = '';
            pageSelectorDiv.appendChild(nextPageButton);
        }
    }
}

class MiniDriver {

    constructor() {
        this.javUrl = new URL(window.location.href);
    }

    execute() {
        if (!this.javUrl.pathname.includes('.php')) {
            // Homepage or video page
            this.javVideoId = this.javUrl.searchParams.get('v');

            // Video page
            if (this.javVideoId != null) {
                this.setEditionNumber();
                this.updateTitle();
                this.addScreenshot();
                this.addTorrentLinks();
                this.updateReviews();
                this.updateComments();
                this.getPreview();
            }
        }
    }

    setEditionNumber() {
        let edition = document.getElementById('video_id').getElementsByClassName('text')[0];
        this.editionNumber = edition.innerText;
    }
    
    async updateTitle() {
        let videoTitle = document.getElementById('video_title');
        let postTitle = videoTitle.getElementsByClassName('post-title')[0];
        postTitle.innerText = postTitle.getElementsByTagName('a')[0].innerText;

        // Add English title 
        if (!window.location.href.includes('/en/')) {
            let request = {url: `http://www.javlibrary.com/en/?v=${this.javVideoId}`};
            let result = await xhrFetch(request).catch(err => {console.log(err); return;});
            let videoDetailsDoc = parseHTMLText(result.responseText);
            let englishTitle = videoDetailsDoc.getElementById('video_title')
                                    .getElementsByClassName('post-title')[0]
                                    .getElementsByTagName('a')[0].innerText;
            postTitle.innerHTML = `${postTitle.innerText}<br/>${englishTitle}`;
        }
    }

    addScreenshot() {
        let imgUrl = `http://javscreens.com/images/${this.editionNumber}.jpg`;
        let img = createElementFromHTML(`<img src="${imgUrl}" id="full_screenshot" title="">`);
        insertAfter(img, document.getElementById('video_favorite_edit'));

        img.addEventListener('click', function() {
            if (img.style.width != '100%') {
                img.style.width = '100%';
            } else {
                img.style.width = '15%';
                if (document.getElementById('full_screenshot').getBoundingClientRect().y < 0) {
                    location.hash = '#full_screenshot';
                    location.href = '#full_screenshot';
                }
            }
        });
    }

    addTorrentLinks() {
        let sukebei = `https://sukebei.nyaa.si/?f=0&c=0_0&q=${this.editionNumber}`;
        let btsow = `https://btsow.pw/search/${this.editionNumber}`;
        let belibrary = `https://www.btlibrary.info/btlibrary/${this.editionNumber}/1-1-1-1.html`;
        let torrentKitty = `https://www.torrentkitty.tv/search/${this.editionNumber}`;
        let tokyotosho = `https://www.tokyotosho.info/search.php?terms=${this.editionNumber}`;
        
        let torrentsHTML = `
            <div id="torrents">
                <form id="form-btkitty" method="post" target="_blank" action="http://btkittyba.co/">
                    <input type="hidden" name="keyword" value="${this.editionNumber}">
                    <input type="hidden" name="hidden" value="true">
                </form>
                <form id="form-btdiggs" method="post" target="_blank" action="http://btdiggba.me/">
                    <input type="hidden" name="keyword" value="${this.editionNumber}">
                </form>
                <table>
                    <tr>
                        <td><strong>种子:</strong></td>
                        <td><a href="${sukebei}" target="_blank">sukebei</a></td>
                        <td><a href="${btsow}" target="_blank">btsow</a></td>
                        <td><a href="${belibrary}" target="_blank">belibrary</a></td>
                        <td><a href="${torrentKitty}" target="_blank">torrentKitty</a></td>
                        <td><a href="${tokyotosho}" target="_blank">tokyotosho</a></td>
                        <td><a id="btkitty" href="JavaScript:Void(0);" onclick="document.getElementById('form-btkitty').submit();">btkitty</a></td>
                        <td><a id="btdiggs" href="JavaScript:Void(0);" onclick="document.getElementById('form-btdiggs').submit();">btdiggs</a></td>
                    </tr>
                </table>
            </div>
        `;

        let torrents = createElementFromHTML(torrentsHTML);
        insertAfter(torrents, document.getElementById('video_favorite_edit'));
    }

    updateReviews() {
        // Remove existing reviews
        let videoReviews = document.getElementById('video_reviews');
        Array.from(videoReviews.children).forEach(child => {
            if (child.id.includes('review')) {
                child.parentNode.removeChild(child);
            }
        });

        // Add all reviews
        this.getReviews(1);
    }

    async getReviews(page) {
        // Update current page
        let loadMoreId = 'load_more_reviews';
        let loadMoreDiv = document.getElementById(loadMoreId);
        if (loadMoreDiv != null) {
            loadMoreDiv.parentNode.removeChild(loadMoreDiv);
        }

        // Load more reviews
        let request = {url: `http://www.javlibrary.com/cn/videoreviews.php?v=${this.javVideoId}&mode=2&page=${page}`};
        let result = await xhrFetch(request).catch(err => {console.log(err); return;});
        let reviewsDoc = parseHTMLText(result.responseText);

        let videoReviews = reviewsDoc.getElementById('video_reviews');
        if (videoReviews.getElementsByClassName('t').length == 0 || 
                reviewsDoc.getElementsByClassName('page_selector').length == 0) {
            return;
        }
        // Set review texts
        Array.from(videoReviews.getElementsByClassName('t')).forEach(review => {
            review.getElementsByClassName('text')[0].innerHTML = parseBBCode(escapeHtml(review.getElementsByTagName('textarea')[0].innerText));
        });

        // Append reviews to the page
        let currentVideoReviews = document.getElementById('video_reviews');
        let bottomLine = currentVideoReviews.getElementsByClassName('grey')[0];
        Array.from(videoReviews.children).forEach(element => {
            if (element.tagName == 'TABLE') {
                currentVideoReviews.insertBefore(element, bottomLine);
            }
        });

        // Append load more if there are more reviews
        let nextPage = reviewsDoc.getElementsByClassName('page next');
        if (nextPage.length > 0) {
            let loadMoreReviews = getLoadMoreButton(loadMoreId, async () => this.getReviews(page + 1));
            insertAfter(loadMoreReviews, currentVideoReviews);
        }
    }

    updateComments() {
        // Remove existing comments
        let videoComments = document.getElementById('video_comments');
        Array.from(videoComments.children).forEach(child => {
            if (child.id.includes('comment')) {
                child.parentNode.removeChild(child);
            }
        });

        // Add all comments
        this.getComments(1);
    }

    async getComments(page) {
        // Update current page
        let loadMoreId = 'load_more_comments';
        let loadMoreDiv = document.getElementById(loadMoreId);
        if (loadMoreDiv != null) {
            loadMoreDiv.parentNode.removeChild(loadMoreDiv);
        }

        // Load more comments
        let request = {url: `http://www.javlibrary.com/cn/videocomments.php?v=${this.javVideoId}&mode=2&page=${page}`};
        let result = await xhrFetch(request).catch(err => {console.log(err); return;});
        let commentsDoc = parseHTMLText(result.responseText);

        let videoComments = commentsDoc.getElementById('video_comments');
        if (videoComments.getElementsByClassName('t').length == 0 || 
                commentsDoc.getElementsByClassName('page_selector').length == 0) {
            return;
        }

        // Set comment texts
        Array.from(videoComments.getElementsByClassName('t')).forEach(comment => {
            comment.getElementsByClassName('text')[0].innerHTML = parseBBCode(escapeHtml(comment.getElementsByTagName('textarea')[0].innerText));
        });

        // Append comments to the page
        let currentVideoComments = document.getElementById('video_comments');
        let bottomLine = currentVideoComments.getElementsByClassName('grey')[0];
        Array.from(videoComments.children).forEach(element => {
            if (element.tagName == 'TABLE') {
                currentVideoComments.insertBefore(element, bottomLine);
            }
        });

        // Append load more if there are more comments
        let nextPage = commentsDoc.getElementsByClassName('page next');
        if (nextPage.length > 0) {
            let loadMoreComments = getLoadMoreButton(loadMoreId, async () => this.getComments(page + 1));
            insertAfter(loadMoreComments, currentVideoComments);
        }
    }

    getPreview() {
        let includesEditionNumber = (str) => {
            return str != null
                    // && str.includes(this.editionNumber.toLowerCase().split('-')[0])
                    && str.includes(this.editionNumber.toLowerCase().split('-')[1]);
        }

        let r18 = async () => {
            let request = {url: `https://www.r18.com/common/search/order=match/searchword='${this.editionNumber}'/`};
            let result = await gmFetch(request).catch(err => {console.log(err); return;});
            let video_tag = parseHTMLText(result.responseText).querySelector('.js-view-sample');
            let src = ['high', 'med', 'low']
                            .map(i => video_tag.getAttribute('data-video-' + i))
                            .find(i => i);
            return src;
        }

        let dmm = async () => {
            // Find dmm cid
            let bingRequest = {url: `https://www.bing.com/search?q=${this.editionNumber.toLowerCase()}+site%3awww.dmm.co.jp`}
            let bingResult = await gmFetch(bingRequest).catch(err => {console.log(err); return;});
            let bingDoc = parseHTMLText(bingResult.responseText);
            let pattern = /(cid=[\w]+|pid=[\w]+)/g;
            let dmmCid = '';
            for (let match of bingDoc.body.innerHTML.match(pattern)) {
                if (includesEditionNumber(match)) {
                    dmmCid = match.replace(/(cid=|pid=)/, '');
                    break;
                }
            }

            if (dmmCid == '') {
                return;
            }

            let request = {url: `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${dmmCid}/mtype=AhRVShI_/service=litevideo/mode=/width=560/height=360/`};
            let result = await gmFetch(request).catch(err => {console.log(err); return;});
            let doc = parseHTMLText(result.responseText);

            // Very hacky... Didn't find a way to parse the HTML with JS.
            for (let script of doc.getElementsByTagName('script')) {
                if (script.innerText != null && script.innerText.includes('.mp4')) {
                    for (let line of script.innerText.split('\n')) {
                        if (line.includes('var params')) {
                            line = line.replace('var params =', '').replace(';', '');
                            let videoSrc = JSON.parse(line).src;
                            if (!videoSrc.startsWith('http')) {
                                videoSrc = 'http:' + videoSrc;
                            }
                            return videoSrc;
                        }
                    }
                }
            }
        }

        let sod = async () => {
            // Adult check
            await gmFetch({url: `https://ec.sod.co.jp/prime/_ontime.php`});

            let request = {url: `https://ec.sod.co.jp/prime/videos/sample.php?id=${this.editionNumber}`};
            let result = await gmFetch(request).catch(err => {console.log(err); return;});
            let doc = parseHTMLText(result.responseText);
            return doc.getElementsByTagName('source')[0].src;
        }

        let jav321 = async () => {
            let request = {
                url: `https://www.jav321.com/search`,
                method: 'POST',
                data: `sn=${this.editionNumber}`,
                headers: {
                    referer: 'https://www.jav321.com/',
                    'content-type': 'application/x-www-form-urlencoded',
                },
            };

            let result = await gmFetch(request).catch(err => {console.log(err); return;});
            let doc = parseHTMLText(result.responseText);
            return doc.getElementsByTagName('source')[0].src;
        }

        let kv = async () => {
            if (this.editionNumber.includes('KV-')) {
                return `http://fskvsample.knights-visual.com/samplemov/${this.editionNumber.toLowerCase()}-samp-st.mp4`;
            }

            return;
        }

        Promise.all(
            [jav321, r18, dmm, sod, kv].map(source => source().catch(err => {console.log(err); return;}))
        ).then(responses => {
            console.log(responses);

            let videoHtml = responses
                                .filter(response => response != null 
                                        && includesEditionNumber(response) 
                                        && !response.includes('//_sample.mp4'))
                                .map(response => `<source src="${response}">`)
                                .join('');
            let previewHtml = `
                <div id="preview">
                    <video controls>
                        ${videoHtml}
                    </video>
                </div>
            `;
            insertAfter(createElementFromHTML(previewHtml), document.getElementById('torrents'));
        });
    }
}

// Adult check
setCookie('over18', 18);

// Style change
addStyle();

new MiniDriver().execute();
new MiniDriverThumbnail().execute();