LImgDLer

Auto-DL

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name     LImgDLer
// @version  1.11
// @grant    GM_download
// @grant    GM_addStyle
// @include  https://*.ladies.de/Sex-Anzeigen/*
// @include  https://ladies.de/Sex-Anzeigen/*
// @include  https://escorts24.de/*
// @connect video1.ladies.de
// @connect ladies.de
// @connect escorts24.de
// @namespace https://greasyfork.org/users/290665
// @description Auto-DL
// ==/UserScript==
/*jslint es6 */
"use strict";
let downloadFolder;
const SCRIPT_VERSION = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version)
    ? String(GM_info.script.version)
    : 'unknown';
let phonews;
let NAMERAW, PHONE;
if (typeof jQuery === 'undefined') {
    console.error('LImgDLer: jQuery not found. Aborting.');
} else {
    jQuery(function () {
        hideSpam();
        setupMutationObserver();
        if (jQuery('.kuenstlername').length) { // modern mode
            NAMERAW = jQuery('.kuenstlername').first().text();
            PHONE = jQuery('.contacts-data strong').first().text();
        } else if (jQuery('.auftrag-name h3').length) { // themenladies
            NAMERAW = jQuery('.auftrag-name h3').first().text();
            PHONE = jQuery('p.telefon strong').first().text();
        } else if (jQuery('div.full_pad.bigfont strong').length) { // classic
            NAMERAW = jQuery('div.full_pad.bigfont strong').text();
            PHONE = jQuery('.div_td_last.lalign.midfont.itxt_pad.icon_text').eq(1).text();
        }
        if (!PHONE || !NAMERAW) {
            console.warn('LImgDLer: some selectors not found; proceeding with defaults.');
        }
        // Coerce and sanitize values, use safe defaults when missing
        PHONE = PHONE ? String(PHONE).replace(/\s\/\s/g, '-') : '';
        let NAME = NAMERAW ? String(NAMERAW).replace(/[^\w-\(\)äöüß+ ]/ig, '') : '';
        downloadFolder = (NAME || PHONE) ? (NAME + ' - ' + PHONE) : 'unknown';
        phonews = PHONE.replace(/[ \-]+/g, '');
        // Build search term defensively (skip empty tokens)
        const _terms = [];
        if (NAME) _terms.push(`\"${NAME}\"`);
        if (PHONE) _terms.push(`\"${PHONE}\"`);
        if (phonews) _terms.push(phonews);
        let SEARCHTERM = _terms.length ? `(${_terms.join(' OR ')})` : '';

        let dlArea = jQuery('<div id="LImgDLer">'
            + '<div class="title"><span class="Llogo">LImgDLer<span class="Lversion">' + SCRIPT_VERSION + '</span></span> '
            + '<button id="dlbutton">📥 Download <b>' + downloadFolder + '</b></button>'
            + '<button id="lhbutton">🔍 search LH</button>'
            + '<button id="copybutton">📋 copy Text</button>'
            + '</div></div>');

        jQuery('body').on('click', '#lhbutton', function (event) {
            event.stopPropagation();
            event.preventDefault();

            let url = 'https://www.google.de/search?as_sitesearch=lusthaus.cc&q=';
            url += encodeURIComponent(SEARCHTERM);
            window.open(url);
        });

        jQuery(dlArea).find('button').data('name', downloadFolder);
        jQuery('body').on('click', '#dlbutton', function (event) {
            startDownloads(jQuery(event.currentTarget).data('name'));
        });
        jQuery('body').on('click', '#copybutton', function (event) {
            try {
                navigator.clipboard.writeText(adData()).catch(function (e) {
                    // older browsers might reject promises
                    throw e;
                });
            } catch (e) {
                // fallback: open a prompt so user can copy manually
                let text = adData();
                try { window.prompt('Copy ad data (Ctrl+C, Enter):', text); } catch (ee) { alert('Copy failed, please copy manually:\n' + text); }
            }
        });

        if (jQuery('.auftrag-title').length) { // modern mode
            addStyle();
            jQuery('.auftrag-title').first().append(dlArea);
        } else if (jQuery('div#content div.container').length) { // themenladies
            jQuery('div#content div.container').first().append(dlArea);
            addStyle("themen");
        } else if (jQuery('div.sitemenu').length) { // classic
            jQuery('div.sitemenu').append(dlArea);
            addStyle("classic");
        }
        jQuery('#lhbutton').attr('title', SEARCHTERM);
    });
}

function startDownloads(folderName) {
    let downloadlist = [];
    if (jQuery('.rsNav img').length) {
        jQuery('.rsNav img').each(function () {
            let link = jQuery(this).attr('src');
            if (link.match(/-FK\./)) {
                return; // skip videos
            }
            link = link.replace(/\?.*/, '');
            downloadlist.push(link.replace(/(\d+)-F(\d+)\./, '$1-A$2.'));
        });
        jQuery('a.gallery_video').each(function () { // get videos
            downloadlist.push(jQuery(this).attr('href'));
        });
        dlAll(folderName, downloadlist);
    } else {
        let images = jQuery('.photo-slide img');
        images.each(function () {
            let imgURL = jQuery(this).attr('src');
            downloadlist.push(imgURL);
        });
        let videos = jQuery('.video-slide lds-video-player');
        videos.each(function () {
            let videoOptions = jQuery(this).attr('options');
            if (videoOptions) {
                try {
                    let options = JSON.parse(videoOptions);
                    if (options && options.sources && options.sources.length) {
                        downloadlist.push(...options.sources);
                    }
                } catch (e) {
                    console.warn('Failed to parse video options:', videoOptions);
                }
            }
        });
        dlAll(folderName, downloadlist);
    }
}

function dlAll(path, downloadlist) {
    jQuery('#LImgDLerdialog').remove();
    const dialog = jQuery('<div id="LImgDLerdialog" title="Download to ' + path + ' ..."></div>').appendTo(jQuery('#LImgDLer'));

    for (let URL of downloadlist) {
        let originalName = (URL || '').replace(/.*\//, '').replace(/\?.*/g, '');
        let filename = originalName || 'file';
        let safeFolder = (path || '').replace(/[^\w\-\(\)\ ]/g, '_');
        // Try folder-style name first (safeFolder/filename) to preserve original behavior.
        // If the userscript manager doesn't support folders, retry with a flat fallback.
        let fileWithSlash = safeFolder ? (safeFolder + '/' + filename) : filename;
        let fallbackFile = safeFolder ? (safeFolder + ' - ' + filename) : filename;
        URL = (URL || '').replace(/^\/\//, 'https://');
        let line = jQuery('<div class="RLDL" data-name="' + filename + '">' + filename + '</div>\n').appendTo(dialog);

        (function (url, initialPath, fallbackPath, filenameLocal, linediv) {
            const attemptDownload = function (filepath, triedFallback) {
                GM_download({
                    url: url,
                    name: filepath,
                    saveAs: false,
                    onerror: function (err) {
                        let errText = 'Unknown error';
                        try {
                            if (!err) errText = 'no error object';
                            else if (err.error) errText = err.error;
                            else if (err.message) errText = err.message;
                            else errText = JSON.stringify(err);
                        } catch (ee) { errText = String(err); }
                        // If we tried the folder path and it failed, retry with fallback once.
                        if (!triedFallback && filepath === initialPath && fallbackPath !== initialPath) {
                            console.warn('LImgDLer: folder-style download failed, retrying flat filename.', err);
                            attemptDownload(fallbackPath, true);
                            return;
                        }
                        jQuery(linediv).append('<span class="download_error">ERROR: ' + errText + '</span>');
                        console.error('LImgDLer download error:', err);
                    },
                    onload: function () {
                        jQuery(linediv).append('<span class="download_ok">✓</span>');
                    }
                });
            };

            // Start with folder-style path, fallback will be attempted automatically on error.
            attemptDownload(fileWithSlash, false);
        })(URL, fileWithSlash, fallbackFile, filename, line);

    }
}
function adData() {
    // Defensive phone formatting (unique variants)
    const raw = (phonews || '').trim();
    const local = raw.replace(/\+49\(0\)/, '0');
    let dash = local.replace(/^(01[67].)/, '$1-');
    dash = dash.replace(/^(015510)/, '$1-');
    dash = dash.replace(/^(015..)(\d+)$/, '$1-$2');
    const intl = raw.replace(/^0([1-9])/, '+49$1');
    const phones = Array.from(new Set([dash, local, intl].filter(Boolean)));

    // Attributes
    const attrs = [];
    jQuery('.attribute-column > .row').each(function () {
        const key = jQuery(this).find('strong').first().text().trim();
        const vals = [];
        jQuery(this).find('p').each(function () { vals.push(jQuery(this).text().trim()); });
        const v = vals.join(', ');
        if (key && v) attrs.push(`${key}: [B]${v}[/B]`);
    });

    // Club and address
    const clubname = jQuery('[property=locationName]').first().text().trim();
    const clublink = jQuery('a[property="ladyLocation"]').attr('href') || '';
    const club = clubname ? `[URL=https://ladies.de${clublink}]${clubname}[/URL]` : '';
    const street = jQuery('[itemprop=address] [property=streetAddress]').first().text() || '';
    const postal = jQuery('[itemprop=address] [itemprop=postalCode]').first().attr('content') || '';
    const city = jQuery('[itemprop=address] [itemprop=addressLocality]').first().text() || jQuery('.contacts-data .address-details').first().text().replace(/[\s\n]+/g, ' ').trim() || '';
    const addressText = [street, postal, city].map(s => (s||'').trim()).filter(Boolean).join(' ');
    const addressBB = addressText ? `[URL=https://www.google.de/maps/place/${encodeURIComponent(addressText)}]${addressText}[/URL]` : '';

    // Description: concatenate multiple blocks if present, keep block separation
    let description = '';
    const descBlocks = [];
    const descContainer = jQuery('.einzelansicht-text div');
    if (descContainer.length) {
        // prefer direct child blocks (div, p, section)
        descContainer.children('div, p, section').each(function () {
            let html = jQuery(this).clone().find('span').remove().end().html() || '';
            // preserve <br> as newlines
            html = html.replace(/<br\s*\/?>/gi, '\n');
            // strip remaining tags
            let txt = html.replace(/<[^>]+>/g, '');
            // decode HTML entities
            try { txt = jQuery('<textarea/>').html(txt).text(); } catch (er) {}
            txt = txt.replace(/\n\s*\n+/g, '\n\n').trim();
            if (txt) descBlocks.push(txt);
        });
        // fallback: if no child blocks, use html content and split paragraphs
        if (!descBlocks.length) {
            const fullHtml = descContainer.html() || '';
            if (fullHtml) {
                let tmp = fullHtml.replace(/<br\s*\/?>/gi, '\n');
                tmp = tmp.replace(/<[^>]+>/g, '');
                try { tmp = jQuery('<textarea/>').html(tmp).text(); } catch (er) {}
                const parts = tmp.split(/\n\s*\n+/).map(s => s.trim()).filter(Boolean);
                if (parts.length) descBlocks.push(...parts);
            }
        }
        description = descBlocks.join('\n\n');
    }

    // Services
    const services = [];
    jQuery('.profil .column-group').each(function () {
        const key = jQuery(this).find('strong').first().text().trim();
        const vals = [];
        jQuery(this).find('.attribute-item').each(function () { vals.push(jQuery(this).text().trim()); });
        const v = vals.join(', ');
        if (key && v) services.push(`${key}: [B]${v}[/B]`);
    });

    const title = NAMERAW || downloadFolder || '';
    const pageURL = window.location.href || '';

    // Build BBCode with German headings, single-line attributes and size wrappers
    const parts = [];
    parts.push('Infoservice');
    parts.push('[QUOTE]');
    if (title) parts.push(`[URL=${pageURL}][B]${title}[/B][/URL]`);
    // size wrapper for compact content
    parts.push('[SIZE="1"]');
    if (description) {
        parts.push(description);
        parts.push('\n'); // ensure separation from next section
    }
    if (attrs.length) {
        parts.push(attrs.join('\n'));
    }
    if (services.length) {
        parts.push('\n[B]Service[/B]');
        parts.push(services.join('\n'));
    }
    if (phones.length) {
        parts.push('\n[B]Telefon[/B]');
        // large primary phone, others inline
        parts.push(`[B][SIZE=3]${phones[0]}[/SIZE][/B]` + (phones.length > 1 ? (', ' + phones.slice(1).join(' / ')) : ''));
    }
    if (club || addressBB) {
        parts.push('\n[B]Adresse[/B]');
        parts.push(((club ? club + ', ' : '') + (addressBB || '')).trim());
    }
    parts.push('[/SIZE]');
    parts.push('[/QUOTE]');

    const dataHTML = parts.join('\n').replace(/ {2,}/g, ' ').replace(/\n{3,}/g, '\n\n');
    return dataHTML;
}
function hideSpam() {
    jQuery('div.anzeige').has('span.webcam-markierung').remove();
    jQuery('div.anzeige').has('div.closed-info').css('opacity', '0.3');
    jQuery('section#page-top-sol').remove();
}
function setupMutationObserver() {
    const observer = new MutationObserver(hideSpam);
    let target = jQuery('ul.pagination');
    if (target.length) {
        observer.observe(jQuery(target).get(0), {
            attributes: true,
            childList: true,
            subtree: true
        });
    }
}

function addStyle(mode) {
    let styles = [];
    styles["classic"] = `
#LImgDLer {
    left: 174px;
    top: 109px;
    right: 202px;
    line-height: 10px;
    font-size:9px;
}
.Llogo {
    font-size: 16px;
    top: 2px;
}
#LImgDLer button {
    padding: 4px 12px;
    font-size:11px;
}
button:hover {
    cursor:pointer;
}
    `;
    styles["themen"] = `
div#content div.container {
  position:relative;
}
#LImgDLer {
    left: 16px;
    top: 50px;
    right: 15px;
    line-height: 10px;
}
.Llogo {
    font-size: 17px;
    top: 1px;
}
#LImgDLer button {
    padding: 4px 12px;
}

    `;

    GM_addStyle(`
.download_error {
    color: white;
    background-color: #880010;
    padding: 1px 4px;
    border-radius: 2px;
    margin: 0px 4px;
}

.download_ok {
    color: white;
    background-color: #10a020;
    padding: 1px 4px;
    border-radius: 2px;
    margin: 0px 4px;
}

#LImgDLer {
    max-height: 180px;
    font-size: 10px;
    position: absolute;
    left: 240px;
    top: 0;
    right: 30px;
}
#LImgDLer button {
    background-color: #e28c13;
    border: none;
    margin: 0px 4px;
    padding: 10px 12px;
    line-height: 15px;
    font-size: 15px;
    color: white;
}
#LImgDLer > div.title {
    background-color:#6f789f;
}

#LImgDLerdialog {
    background-color: rgba(255, 255, 255, .52);
    overflow: auto;
    column-width: 110px;
    padding:4px;
}
.Llogo {
    padding: 0 12px;
    color: #e69536;
    font-size: 24px;
    font-weight: 700;
    text-shadow: 0 0 1.5px black;
    font-style: italic;
    top: 3px;
    position: relative;
    line-height: 12px;
}
.Lversion {
    position: absolute;
    top: -6px;
    right: -6px;
    background: #9ba4cc;
    color: white;
    padding: 2px 6px;
    border-radius: 10px;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    box-shadow: 1px 2px 6px #00000050;
}
`);
    if (mode && styles[mode]) {
        GM_addStyle(styles[mode]);
    }
}