プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

アイドルギャラリーやぷちデレラ等のJSONを云々

概要

モバマスのサ終によせて。
色々埋め状況をチェックしたり、保存したい方向け

前提

基本、ブラウザUA弄ってモバマス遊んでる歴戦のP向け。
PC版アプリでも DevTools 開ければいけるかも?

F12DevTools を起動して、console の箇所で各種スクリプトをコピペ実行できる方。

前置き・基礎

普通にギャラリーから開くとクエリがつくのでクソ長URLになる。
不要な部分を削るとレイアウトがおかしくなる事があるが、
それは多分ブラウザが勝手に https で繋ごうとしているからで、 http で明示的に繋ぎなおせば解決する

URL例:[こころに春を]高森藍子
https://sp.pf.mbga.jp/12008305/?guid=ON&url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fidol_gallery%2Fidol_detail%2F1cdd2ddaf342338e11ecd5fe7990466e

URL部分 idol_detail%2F に続くハッシュがカード管理ID

アイドルオブジェクトの構成

💡 大雑把な構成

+ "detail_list" ( Array )
    + カードデータオブジェクトA
    + カードデータオブジェクトB..
    + カードデータオブジェクトC...
+ "idol_story_list" ( Array )
    + エピソードアクセスオブジェクトA
    + エピソードアクセスオブジェクトB..
    + エピソードアクセスオブジェクトA...
+ "images" ( object )
    + "bg"
    + "l"
    + "l_premium"
    + "l_nosign"
    + "l_nosign_p"
    + "l_noframe"
    + "mask_l"
    + "mask_l2"
    + "mask_l_p"
    + "mask_l2_p"
    + "xs"
    + "m2"
    + "quest"
+ "ios_app_flg"
+ "apple_review"

💡 もう少し掘り下げる

{
    "detail_list":[
        {
            "data": {
                "card_name": "[接頭辞]アイドル名",
                "rarity": "レアリティ",
                "cost": "コスト",
                "attribute": "属性名/英語表記の模様",
                "skill_name": "特技名",
                "skill_effect": "説明",
                "grow_idol_flag": "",
                "default_attack": "攻ステ",
                "default_defense": "守ステ",
                "hash_card_id": "画像ハッシュ",
                "alias_name": "",
                "real_name": "純アイドル名(接頭辞無し)"
            },      
            "profile": {
                "card_id": "カードID数値",
                "card_name": "[接頭辞]アイドル名",
                "card_kana": "ひらがな表記",
                "card_age": "年齢",
                "card_from": "出身",
                "card_height": "身長",
                "card_weight": "体重",
                "card_bust": "バスト",
                "card_waist": "ウェスト",
                "card_hip": "ヒップ",
                "card_birthday": "誕生日",
                "card_constellation": "星座",
                "card_blood": "血液型",
                "card_arm": "利き手",
                "card_hobby": "趣味",
                "card_cv": "担当声優"
            },
            "comments": {
                "comment": "アイドルコメント",
                "comments_my_1": "マイスタジオ",
                "comments_my_2": "マイスタジオ",
                "comments_my_3": "マイスタジオ",
                "comments_my_4": "マイスタジオ",
                "comments_my_max": "親愛度MAXセリフ",
                "comments_work_1": "お仕事",
                "comments_work_2": "お仕事",
                "comments_work_3": "お仕事",
                "comments_work_4": "お仕事",
                "comments_work_max": "お仕事(親愛度MAX)",
                "comments_work_love_up": "お仕事中の親愛UPセリフ",
                "comments_live": "LIVEバトル",
                "comments_love_max": "親愛度MAX演出"
            },
            "voice": {
                "comment": "URL.mp4",
                "comments_my_1": "URL.mp4"",
                "comments_my_2": "URL.mp4"",
                "comments_my_3": "URL.mp4"",
                "comments_my_4": "URL.mp4"",
                "comments_my_max": "URL.mp4"",
                "comments_work_1": "URL.mp4"",
                "comments_work_2": "URL.mp4"",
                "comments_work_3": "URL.mp4"",
                "comments_work_4": "URL.mp4"",
                "comments_work_max": "URL.mp4"
                "comments_work_love_up": "URL.mp4"",
                "comments_live": "URL.mp4"",
                "comments_love_max": "URL.mp4""
            },
            // 入手イベント。ない場合は value 自体が null
            "event": {
                "イベントID数値": {
                    "event_id": イベントID数値,
                    "category_id": "カテゴリID",
                    "event_name": "開催イベント名称"
                }
            },
            "release": "",
            "trade_prohibition": {
                "is_trade_limit": false
            },
            // 検索用のクエリだと思われる
            "idol_search_param": "keyword=エンコード済み文字列",
            // 所持或いは解放
            "archive": {
                "normal": "1",  // 通常サイン。 1 で所持経験あり
                "premium": "0"  // プレミアムサイン。 1 で所持経験あり
            },
            // アルバム登録 および 親愛度MAX経験の有無
            "is_exist_archive": true,
            "is_max_love": true
        }
        ,
        {
            //上記のようなひと固まりのオブジェクトが以下複数個続く
        }
    ],
    // コミュエピソード系
    "idol_story_list":[
        {
            "story_id" : "番号" ,
            "story_title" : "開催ガチャ名",
            "voice_enable" : {  },
            "flash_path" : {    },
            "voice_url" : { },
            "open_flag" : { }, // Array または object
            "icon" : {  },
            "movie_name_voice" : {  },
        },
        {
            //上記のようなひと固まりのオブジェクトが以下複数個続く
        }
    ]
    ,
    // 画像URL群
    "images": {
        "bg":{
        },
        "l":[
        ],
        "l_premium": {
            // オブジェクト key-value ペア
            //数字文字列 ⇔ プレミアムサイン画像URL
        },
        "l_nosign": [
            //url string (Emptyあり)
        ],
        "l_nosign_p": {
            // オブジェクト key-value ペア
            //数字文字列 ⇔ 画像URL
        },
        "l_noframe": [
            // 要素はフレーム無し url (空文字列/nullの可能性あり)
        ],
        "mask_l": [
            // マスク画像関係? 空文字列ばかりのケースが目立つ
        ],
        "mask_l_p": [
            // マスク画像関係? 自環境だと配列の中身は空
        ],
        "mask_l2_p": [
            // マスク画像関係? 自環境だと配列の中身は空
        ],
        "xs": [
            // 要素は正方形型ミニサイズのアイドル顔部分画像 url
            // ユニットメンバーのサムネイル画像的なアレ
        ],
        "m2": [
            // 一回り小さめサイズ?なアイドルカード画像 url
            // 空文字の可能性あり
        ]
        "quest": [
            // 営業イベントお仕事中に表示されるアイドルカード画像 url
            // 空文字の可能性あり
        ]
    },
    "ios_app_flg": false,   // 環境によりそう
    "apple_review": null    // 環境によりそう
}

script等

【重要】 スクリプトはいずれも、アイドルの各種個別ギャラリーページ上で実行してください

また、console.log が潰されているので、内容物の確認は console.info で基本代用しています。
MDN - console

個別アイドルの .json 即時ダウンロード

【実行場所】アイドルギャラリー(個人)

{
    const _data = idol;
    const _blob = new Blob([JSON.stringify(_data,null,'\t')],{type:'application\/json'});
    const _url = URL.createObjectURL(_blob);
    const _link = document.createElement("a");
    const _name = idol?.detail_list[0]?.data?.real_name; 
    _link.href = _url;
    _link.download = `${_name}.json`;
    document.body.appendChild(_link);
    _link.click();
    document.body.removeChild(_link);
    URL.revokeObjectURL(_url);
}

小ネタ / アイドルギャラリーページ

💡 アルバム未登録 / 親愛未開放数確認 【実行場所】アイドルギャラリー(個人) ※ 各アイドルギャラリーの右側に載っている情報見たほうが早い
{
    const _detail_list = idol?.detail_list;
    const _idolName = idol?.detail_list[0]?.profile?.card_name;
    let _noArchive = 0;
    let _noMaxLove = 0;
    _detail_list.forEach(_e=>{
        if(!_e?.is_exist_archive)
            _noArchive++;
        if(!_e?.is_max_love)
            _noMaxLove++;
    });
    console.info(`${_idolName} : アルバム未登録${_noArchive}枚 / 親愛埋め未開放${_noMaxLove}枚`);
}
💡 特定の文字列を含むカード名のみオブジェクトを確認 【実行場所】アイドルギャラリー(個人) 特定のカード名に関するものだけDevTools上で眺めたければ、 例えば「新春」がカード名に入っているものだけピックアップ
{
    const _detail_list = idol?.detail_list;
    _detail_list.forEach(_e=>{
        if(_e?.data?.card_name?.indexOf("新春") != -1){
            console.dir(_e);
        }
    });
}
💡 エピソード情報のオブジェクトを確認 【実行場所】アイドルギャラリー(個人) エピソード(ストーリー)情報のオブジェクト一覧だけ確認 **ただし、ここで得られるエピソード再生先URLは認証情報が必要な為、 そのアカウントで実際にエピソードを開放していないと、アクセスしても見られない。** しかもここで得られるURLは、後の内部処理で書き換えられる事が前提になっている模様。
{
    const _detail_list = idol?.idol_story_list;
    _detail_list.forEach(_e=>{
        console.info(_e);
        console.info(_e?.story_title);
        console.info(_e?.flash_path);
    });
}
💡 アルバム親愛埋め状況をテーブルとして確認 【実行場所】アイドルギャラリー(個人)
{
    const _tableObj = {};
    idol?.detail_list?.forEach(_e=>{
        const _valueObj = {};
        let _unArchiveCount = 0;
        if(!_e?.is_exist_archive){
            _unArchiveCount++;
        }
        if(!_e?.is_max_love){
            _unArchiveCount++;
        }
        const _acvDef = ["💯","👍","💦"];
        const _rareDef = ["?" , "N","N+","R","R+","SR","SR+"];
        _valueObj["コスト"] = _e?.data?.cost;
        _valueObj["レア"] = _rareDef[_e?.data?.rarity];
        _valueObj["埋め"] = _acvDef[_unArchiveCount];
        _valueObj["アルバム登録"] = _e?.is_exist_archive?"📗":null;
        _valueObj["親愛度MAX"] = _e?.is_max_love?"💗":null;
        _valueObj["hash"] = _e?.data?.hash_card_id;
        _tableObj[_e?.data?.card_name] = _valueObj;
    });
    console.table(_tableObj);
}
💡 DL可能なボイス数などを Console.table() で確認 【実行場所】アイドルギャラリー(個人) 色々情報を載せようとしてかなりカオスなコードに...
{
    const _tableInfoObj = {};
    let _unDlVoiceSum = 0;
    let _dlVoiceSum = 0;
    const _detail_list = idol?.detail_list;
    for(const _index in _detail_list){
        const _e = _detail_list[_index];
        // そのカードの cv 情報で判断
        let _noVoice = false;
        if(_e?.profile?.card_cv == null){
            _noVoice = true;
        }
        const _comments = _e?.comments;
        const _tableValueObj = {};
        let _dlVoiceCount = 0;
        let _unDlVoiceCount = 0;
        _tableValueObj["全回収可能?"] = null;
        _tableValueObj["ボイス実装の有無"] = (_noVoice?"未実装":"👌");
        const _voices = _e?.voice;
        let _commentreleasedCount = 0;
        let _allCommentRelease = true;
        for(const _v in _voices){
            if(_comments[_v] === "???"){
                // コメント未開放
                _allCommentRelease = false;
                if(!_noVoice){
                    _unDlVoiceSum++;
                    _unDlVoiceCount++;
                }
            }
            else{
                _commentreleasedCount++;
                if(!_noVoice){
                    _dlVoiceCount++;
                }
            }
        }
        const _voiceAllCount = Object.keys(_voices).length;
        _dlVoiceSum += _dlVoiceCount;
        _tableValueObj["DL出来ない未開放ボイス数"] = _noVoice?"未実装":(_unDlVoiceCount);
        _tableValueObj["DL可能ボイス数"] = _noVoice?"未実装":(_dlVoiceCount);
        _tableValueObj["コメント開放数"] = _commentreleasedCount;
        _tableValueObj["コメント未開放数"] = _voiceAllCount - _commentreleasedCount;
        if(_noVoice){
            _tableValueObj["全回収可能?"] = "未実装";
        }
        else{
            _tableValueObj["全回収可能?"] = (_allCommentRelease&&!_noVoice)?"👍全開放":`${_commentreleasedCount}個開放`;
        }
        _tableInfoObj[_e?.data?.card_name] = _tableValueObj;
    }
    console.table(_tableInfoObj);
    console.info(`全カード数 ${Object.keys(_tableInfoObj).length}`);
    console.info(`DL可能なボイス合計 ${_dlVoiceSum} / 実装済だが未開放でDL出来ないボイス合計 ${_unDlVoiceSum}`);
}

ボイスファイル一括(順次)DL

【実行場所】アイドルギャラリー(個人)

本丸。 公開しておいて何ですが、用法容量を守って適切にご利用ください...
泥臭い処理ですがユルシテ
中断はページ上でEscキーを押せばOKなハズ。

  • _dlResumeIndex はレジューム(途中からDLする)機能相当。
    DL予定となるファイル数序列の数字を代入しておくと、
    その序列以前にDLするはずだったファイルを全てスキップして、連番途中から開始する

予約DLがすべて完了した場合、任意のオーディオファイルを鳴らす機能も加えました。

DLファイル名は [カード名]_[コメント役割を示す英語のキー名].mp4 的な感じにしています。
後からリネームするのクッソ面倒だと思うんで、
オブジェクトとか眺めて改造できるなら、前もって任意でフォーマット変えておいた方が良いかもしれません


💡 ボイスファイル一括(順次)DL / クリックで展開
{
    // レジューム機能 ( DL予約一覧の序列番号を代入 )
    // 0 だと最初から
    const _dlResumeIndex = 0; 
    const _dlTargetDefObj = [];
    let _unDlVoiceSum = 0;
    let _dlVoiceSum = 0;
    window.voiceFileDlStop = false;
    const _detail_list = idol?.detail_list;
    for(const _index in _detail_list){
        const _e = _detail_list[_index];
        let _noVoice = false;
        if(_e?.profile?.card_cv == null){
            _noVoice = true;
        }
        const _comments = _e?.comments;
        const _voices = _e?.voice;
        for(const _v in _voices){
            if(_comments[_v] === "???"){
                if(!_noVoice){
                    _unDlVoiceSum++;
                }
            }
            else{
                if(!_noVoice){
                    _dlVoiceSum++;
                    const _url = _voices[_v];
                    // ファイル名基本フォーマット (拡張子除く)
                    const _saveFileFormatName = `${_e?.data?.card_name}_${_v}`;
                    let _isSkip = false;
                    if(_dlResumeIndex > _dlVoiceSum){
                        _isSkip = true;
                    }
                    _dlTargetDefObj.push( { "filename" : _saveFileFormatName , "url" : _url , "skip" : _isSkip} );
                }
            }            
        }
    }
    console.info("=================================================");
    console.info("DL処理を開始します。 中止したい場合はページ上で Esc(エスケープキー) を押してください");
    console.info("=================================================");
    console.info(`DL予定のボイス数合計 :  ${_dlVoiceSum}`);
    console.info(`DL出来ない未開放コメントの実装済ボイス数合計 ${_unDlVoiceSum}`)

    function _fileDl(_dlInfo , _index) {
        return new Promise((resolve,reject) => {
            const _fileName = _dlInfo.filename;
            const _url = _dlInfo.url;
            const _skip = _dlInfo.skip;
            if(window.voiceFileDlStop){
                console.info("以降のDLを全て中止します");
                reject();
                return;
            }
            if(_skip){
                console.info(`${_fileName} (${_index+1}個目) のDLをスキップします`);
                resolve();
                return;
            }
            setTimeout(() => {
                const _req = new XMLHttpRequest();
                _req.open("GET", _url , true);
                _req.responseType = "blob";
                _req.onload = function(oEvent) {
                    const blob = _req.response;
                    const _dlUrl = URL.createObjectURL(blob);
                    const _ext = _url.split('.').pop();
                    console.info(`${_dlInfo.filename} : ${blob.size} byte`);
                    {
                        const _a = document.createElement('a');
                        _a.href = _dlUrl;
                        // ファイル名+拡張子
                        _a.download = `${_fileName}.${_ext}`;
                        document.body.appendChild(_a);
                        _a.click();
                        document.body.removeChild(_a);
                        URL.revokeObjectURL(_dlUrl);
                        console.info(`${_index+1} 個目のDLが完了しました`);
                    }
                    resolve();
                };
                _req.send();
            }, 500);    // DL間隔ミリ秒。あまり縮めないほうが良い
        });
    }

    (async () => {
        await _dlTargetDefObj.reduce((promise, _dlInfo , _currentIndex) => {
            return promise.then(async () => {
                await _fileDl(_dlInfo , _currentIndex);
            });
        }, Promise.resolve());
        console.info("=== 予約中のボイスデータを全てDLしたと判断されました ====");
        // 全てのDL処理完了時に鳴らしたい処理(+そのオーディオファイル)
        // ぶっちゃけ無くても良い
        {
            const _file = "https://resource.mobamas.net/idolmaster/sound/3207601/637e9bda8de8a884cf5ee0f9b695d5f8.mp4";
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();  
            fetch(_file).then(_res=>{
                return _res.arrayBuffer();
            }).then(_arrayBuffer => {
                audioContext.decodeAudioData(_arrayBuffer , _audioBuffer =>{
                    const _source = audioContext.createBufferSource();
                    _source.buffer = _audioBuffer;
                    const _gainNode = audioContext.createGain();
                    _source.connect(_gainNode);
                    _gainNode.connect(audioContext.destination);
                    _gainNode.gain.value =  0.2;    // 音量
                    _source.start(0);
                });
            });
        }
    })();
    // DL中止用
    document.body.addEventListener("keydown" , e=>{
        if (e.code === "Escape") {
            window.voiceFileDlStop = true;
        }
    });
}


Promise の順次実行コードにかなり助けられました。 感謝✨
Zenn > wintyo > 配列のPromiseを直列で実行する方法

一部演出ページのFlashを自動名前付けDL

【実行場所】エピソード再生ページ

一般的な(古めの)ストーリー再生ページで実行すると、 .swf で作成された物ならファイル名付きでDL可能なはず。
アイドルギャラリーから観られる(古い)エピソードを始め、シンデレラヒストリーにも一応対応させている筈
( シンデレラヒストリーも古めの物でないと.swfとしてDL出来ない )
ぷちエピソードは未対応。専用処理として別に組んでいます

DL時のファイル命名、適当に弄ってください。
[アイドル名]_[ストーリーID]_[エピソード話数].swf
[アイドル名]_[ストーリーID]_[エピソード話数]_voice.swf

【※】hash として扱われているものは、そのエピソードアイドルを開く直前にアクセスしたカードの実装カードハッシュな模様

※ APIを挟む事で、初期ハッシュからアイドル名にファイルフォーマットを変更 ※ ボイスアタッチ有無の形式に対応

(async function(){
    // [1] .swf DL用のURL抜き出し(恐らく認証クエリ付き)
    let _swfUrl;
    [...document.scripts].some((_val,_index)=>{
        if(_val.textContent.length > 200){  //一定数の長さで弾く(雑)
            let _pexMatchArr = _val.textContent.match(/new Pex\('(https:[^']*)'/);
            if(_pexMatchArr){
                _swfUrl = _pexMatchArr[1];
            }
        }
    });
    if(!_swfUrl){
        console.info("【!】.swfのDLが出来ないタイプと判断されました");
        return;
    }
    // [2] 自動DL時、ファイル名を付けるための処理。
    // URLからアイドルハッシュ,ストーリー管理番号,エピソード番号を抽出しつつ、.swfとしてDL可能か(こちらでも)精査
    // ガチャRとかの複数エピは後半が %3F だった模様
    const _dlFlashMatch = location.href.match(/flashSsSs(?<story_id>[0-9]{1,})SsSs(?<episode>[0-9]{1,})SsSs[0-9]SsSsidol_gallery--idol_detail--(?<hash>.*)--[0-9]SsSs(?<voice>[0-9]?)%(2F|3F)/);
    let _isCinHis , _doSwfDL , _isVoiceEnable = false;
    let _paramObj;
    if(_dlFlashMatch){
        _paramObj = _dlFlashMatch?.groups;
        _doSwfDL = true;
        _isVoiceEnable = _paramObj?.voice > 0;
    }
    else{
        const _dlInfoMatch = location.href.match(/idol_story%2F(?<type>[^%]+)%2F(?<story_id>[0-9]{1,})%2F(?<episode>[0-9]{1,})[^--].+--idol_detail--(?<hash>.*)--[0-9]%2F(?<voice>[0-9])%/);
        if(_dlInfoMatch){
            _paramObj = _dlInfoMatch?.groups;
            if(_paramObj?.type == "cjs"){
                _doSwfDL = true;
                _isVoiceEnable = _paramObj?.voice > 0;
            }
        }
        // シンデレラヒストリーでは 3から始まる7桁?のIDをストーリーIDとして処理 例->3000005
        const _dlCinHisMatch = location.href.match(/play_episodeSsSs([0-9]{1,})SsSs(?<story_id>[0-9]{1,})SsSs[0-9]+SsSs/);
        if(_dlCinHisMatch){
            _isCinHis = true;
            _paramObj = _dlCinHisMatch?.groups;
            console.info("シンデレラヒストリー再生ページと判断");
            _doSwfDL = true;
        }
    }
    if(!_paramObj){
        console.info("【!】URLを用いたDLパラメタの生成に失敗");
        return;
    }
    //自前API挟んでアイドル名取得
    const _idolJson = await fetch(`https://mkt.packetroom.net/idoldata/?hash=${_paramObj?.hash}`).then(res => res.json());
    console.info(`${_idolJson?.name} / ${_paramObj?.hash}`);
    const _saveIdolName = `${_idolJson?.name?.replace(/\[[^\]]*\]/g,"").replace("+","")}`;  // [肩書]と+ を除去
    console.info(`${_idolJson?.name} → ${_saveIdolName}`);

    if(_doSwfDL){ // .swf DL
        const _blob = await fetch(_swfUrl).then(response => response.blob());
        if(_blob.type.indexOf("flash") != -1){
            const _dlUrl = URL.createObjectURL(_blob);
            const _a = document.createElement('a');
            _a.href = _dlUrl;
            if(_isCinHis){ // シンデレラヒストリー用
                _a.download = `cinderella_history_${_paramObj?.story_id}`;
            }else{      // (旧:アイドル実装初期キャラハッシュ)→(改:アイドル名)+ストーリーID+エピソード数(前,あれば中,後編)+拡張子
                //_a.download = `${_paramObj?.hash}_${_paramObj?.story_id}_${_paramObj?.episode}`;
                _a.download = `${_saveIdolName}_${_paramObj?.story_id}_${_paramObj?.episode}`;

            }
            if(_isVoiceEnable){
                _a.download += `_voice`;
            }
            if(_a.download){
                _a.download += `.swf`;
            }
            document.body.appendChild(_a);
            _a.click();
            document.body.removeChild(_a);
            URL.revokeObjectURL(_dlUrl);
        }
    }
    else{
        console.info("【!】.swfではDL出来ないタイプと判断されました。");
        // ここに .swf でDL出来なかった以外の処理を仕込んでもOK
    }
    console.info(_paramObj);
    console.info(`DL動作 : ${_doSwfDL}`); 
})();
💡 小ネタ / アイドルエピソード(仮称)のURL 3パターン アイドルエピソード(仮称) を開くURLには、確認している限り3つパターンがある模様。 以下パターン例

+ **smart_phone_flash/convert/idol_story**
  + **古めのDL可能なFlash(.swf)で用いられる。 DL可 👍**
  + [放課後サマー]高森藍子 (放課後サマー / 前+後編)
  + [春、神和ぎ]渋谷凛 (新春!光り満ちる振袖 / 前編)
  + [シンデレラガール]渋谷凛 (シンデレラガール渋谷凛 / 前+後編)
  + [楽園の佳景]藤原肇 (かがやく盛夏♪パラダイスサマー / 前+後編)
+ **idol_story/generator_cjs**
  + **疑似Flashエピソードにおける「前編」で多く用いられる。 DL可 👍**
  + [水辺の彩花]櫻井桃華 (水辺の彩花 / 前編)
  + [内緒の休息日]高森藍子 (鮮やかに色づいて 癒しの露天ツアー / 前編)
  + [聖夜の秘めごと]藤原肇 (10周年記念クリスマスパーティー / 前編)
+ **idol_story/cjs**
  + **疑似Flashエピソードにおける「後編」で多く用いられる。 .swf DL不可 💦**
  + [内緒の休息日]高森藍子 (鮮やかに色づいて 癒しの露天ツアー / 後編)
  + [ちいさな手のひら]佐城雪美 (ちいさな手のひら / 後編)

【ぷちデレラ】

ぷち衣装 解放済み一覧 .json 即時DL

解放(所持)済みぷち衣装の情報をDL
.json 形式の筈

【実行場所】ぷちデレラTOP(とか?)

$.ajax({
type: 'POST',
url: 'https://sp.pf.mbga.jp/12008305/?guid=ON&amp;url=http%3A%2F%2F125.6.169.35%2Fidolmaster%2Fpetit_cg%2Fajax_coordinate_list',
dataType: 'json',
data: {
"type": "0",
"deck_position": "1"
}
}).done(
    function(data) {
        var _orgiList = data.accessory_list;
        var _putArr = [];
        for(var n in _orgiList){
            _orgiList[n].info["data_memo"] = "ajax_coordinate_list";
            _putArr.push(_orgiList[n].info);
        }
        var content = JSON.stringify(_putArr , null, "\t"); //整形
        var blob = new Blob([ content ], { "type" : "application\/json" });
        var _link = document.createElement("a");
        _link.href = window.URL.createObjectURL(blob);
        _link.download = "petit.json";
        _link.target = "_blank";
        document.body.appendChild(_link);
        _link.click();
        document.body.removeChild(_link);
    }
);

ぷちセリフ一覧 .json 即時DL

【実行場所】ぷちプロフ(個人)

ぷちデレラ Lv30、かつテクニカルボードを全て開放していれば 取得可能。
( この条件が満たされてなければ null となって得られないぽい )

[ぷちID]_[ショートアイドル名]_petit_serif.json

{
    const _data = comment_list;
    const _blob = new Blob([JSON.stringify(_data,null,'\t')],{type:'application\/json'});
    const _url = URL.createObjectURL(_blob);
    const _link = document.createElement("a");
    const _name = mc.character['1'].short_name;
    const _petitId = mc.character['1'].idol_id;
    _link.href = _url;
    _link.download = `${_petitId}_${_name}_petit_serif.json`;
    document.body.appendChild(_link);
    _link.click();
    document.body.removeChild(_link);
    URL.revokeObjectURL(_url);
}
💡 小ネタメモ / セリフオブジェクト 各ぷちデレラ個人(?)の「ぷちプロフ」ページ上にて、 `comment_list` ( window.comment_list 格納) のconsole実行でセリフ情報が確認できる。 以下、`comment_list` から得られるセリフテキストの雑な趣旨抜粋


+ top (ぷちTOP)
  + Lv1~10 → 5種?
  + Lv1~30 → 7種?
  + Lv11~30 → 4種?
  + Lv11~20 → 4種?
  + Lv21~30 → 13種?
+ lesson (ぷちレッスン)
  + Lv1~10
    + ベースレッスン → 1種?
    + ぷちレッスン → 1種?
    + テクニカルレッスン Vo → 1種?
    + テクニカルレッスン Da → 1種?
    + テクニカルレッスン Vi → 1種?
  + Lv1~30
    + ベースレッスン → 2種?
    + テクニカルレッスン Vo → 1種?
    + テクニカルレッスン Da → 1種?
    + テクニカルレッスン Vi → 1種?
  + Lv11~20
    + ベースレッスン → 2種?
    + ぷちレッスン → 1種?
    + テクニカルレッスン Vo → 1種?
    + テクニカルレッスン Da → 1種?
    + テクニカルレッスン Vi → 1種?
  + Lv21~30
    + ベースレッスン → 2種?
    + ぷちレッスン → 1種?
    + テクニカルレッスン Vo → 2種?
    + テクニカルレッスン Da → 2種?
    + テクニカルレッスン Vi → 2種?
+ shop (ぷちショップ)
  + Lv1~30 → 1種?
+ coordinate (コーディネート)
  + Lv1~30 → 2種?
+ tech_board (テクニカルボード)
  + 開放コメント
    + Lv1~30 → 6種?
  + テクニカルボード
    + Lv1~30 → 3種?


ぷちエピソードの .swf を順次DL

【実行場所】ぷちプロフ(個人)

DLフォーマットは2つ。 ボイスオプションがある場合は、後者のフォーマットもDL
[ぷちID]_[ショートアイドル名]_[エピソード話数].json
[ぷちID]_[ショートアイドル名]_[エピソード話数]_voice.json

{
    (async()=>{
        // ① APIからそのアイドルのぷちエピソード一覧取得
        const _url = "https://sp.pf.mbga.jp/12008305/?guid=ON&amp;url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fpetit_cg%2Fajax_episode_list";
        const _petitId = mc.character['1']?.idol_id;
        const _name = mc.character['1']?.short_name;
        const _formData = new FormData();
        _formData.append('idol_id', _petitId);
        const _epData = await fetch(_url,{ method: "POST",body: _formData
        }).then(_res=>{
            return _res.json();
        });
        console.info(_epData);
        // ② 一覧を精査し、ファイル名フォーマットなどを一気に決める
        const _dlTargetDefObj = [];
        const _ep_list = _epData?.episode_list;
        for(const _index in _ep_list){
            const _e = _ep_list[_index];
            const _epNum = _e?.episode_id;
            const _epName = _e?.episode_name;
            const _epVoice = _e?.episode_voice;
            // ファイル名基本フォーマット (拡張子除く)
            let _saveFileFormatName = `${_petitId}_${_name}_${_epNum}`;
            // ボイス無しURL
            {
                const _voiceOffSwfUrl = `https://sp.pf.mbga.jp/12008305/?guid=ON&amp;url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fsmart_phone_flash`
                +`%2Fconvert%2Fpetit_cgSsSspetit_show_episode_swfSsSs${_petitId}SsSs${_epNum}SsSscoordinate_idolSsSs1%2F0%2F0%2F0%2F1%2F0%2F0%2F0%2F1`
                _dlTargetDefObj.push( { "filename" : _saveFileFormatName , "url" : _voiceOffSwfUrl } );
            }
            // ボイスオプションが存在する場合「あり版」も追加でセット。 判定はこの条件でOK?
            if(_epVoice && _epVoice > 0){   
                _saveFileFormatName += "_voice";
                const _voiceOnSwfUrl = `https://sp.pf.mbga.jp/12008305/?guid=ON&amp;url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fsmart_phone_flash`
                +`%2Fconvert%2Fpetit_cgSsSspetit_show_episode_swfSsSs${_petitId}SsSs${_epNum}SsSscoordinate_idolSsSs1%2F0%2F0%2F0%2F1%2F${_epVoice}%2F0%2F0%2F1`
                _dlTargetDefObj.push( { "filename" : _saveFileFormatName , "url" : _voiceOnSwfUrl } );
            }
        }
        console.info("=================================================");
        console.info("DL処理を開始");
        console.info("=================================================");

        console.info(_dlTargetDefObj);
        // ③ 先にDL処理を定義
        function _fileDl(_dlInfo , _index) {
            return new Promise((resolve,reject) => {
                const _fileName = _dlInfo.filename;
                const _url = _dlInfo.url;
                setTimeout(() => {
                    const _req = new XMLHttpRequest();
                    _req.open("GET", _url , true);
                    _req.responseType = "blob";
                    _req.onload = function(oEvent) {
                        const blob = _req.response;
                        const _dlUrl = URL.createObjectURL(blob);
                        console.info(`${_dlInfo.filename} : ${blob.size} byte`);
                        {
                            const _a = document.createElement('a');
                            _a.href = _dlUrl;
                            // ファイル名+拡張子
                            _a.download = `${_fileName}.swf`;
                            document.body.appendChild(_a);
                            _a.click();
                            document.body.removeChild(_a);
                            URL.revokeObjectURL(_dlUrl);
                            console.info(`${_index+1} 個目のDLが完了しました`);
                        }
                        resolve();
                    };
                    _req.send();
                }, 500);    // DL間隔ミリ秒。あまり縮めないほうが良い
            });
        }
        // ④ DL用定義を用いて順次DL
        (async () => {
            await _dlTargetDefObj.reduce((promise, _dlInfo , _currentIndex) => {
                return promise.then(async () => {
                    await _fileDl(_dlInfo , _currentIndex);
                });
            }, Promise.resolve());
            console.info("=== ぷちエピソードのswfを全てDLしたと判断されました ====");
            // 全てのDL処理完了時に鳴らしたい処理(+そのオーディオファイル)
            // ぶっちゃけ無くても良い
            {
                const _file = "https://resource.mobamas.net/idolmaster/sound/3207601/637e9bda8de8a884cf5ee0f9b695d5f8.mp4";
                const audioContext = new (window.AudioContext || window.webkitAudioContext)();  
                fetch(_file).then(_res=>{
                    return _res.arrayBuffer();
                }).then(_arrayBuffer => {
                    audioContext.decodeAudioData(_arrayBuffer , _audioBuffer =>{
                        const _source = audioContext.createBufferSource();
                        _source.buffer = _audioBuffer;
                        const _gainNode = audioContext.createGain();
                        _source.connect(_gainNode);
                        _gainNode.connect(audioContext.destination);
                        _gainNode.gain.value =  0.2;    // 音量
                        _source.start(0);
                    });
                });
            }
        })();

    })();
}
💡 小ネタメモ / ぷちエピソード一覧 .json 即時DL 【実行場所】ぷちプロフ(個人) **ぷちデレラID_ショートアイドル名_petit_episode.json** ※ 大した情報は含まれていない。
{
    (()=>{
        const _petitId = mc.character['1']?.idol_id;
        const _name = mc.character['1']?.short_name;
        $.ajax({
            type: 'POST',
            url: 'https://sp.pf.mbga.jp/12008305/?guid=ON&url=http%3A%2F%2Fmobamas.net%2Fidolmaster%2Fpetit_cg%2Fajax_episode_list%3Fl_frm%3DPetit_cg_coordinate_idol_1%26rnd%3D933716054',
            dataType: 'json',
            data: {
                'idol_id': _petitId
            }
        })
        .done((data)=>{
            console.info(data);
            // 確認だけしたければここを無効に
            if(false){
                const _data = data;
                const _blob = new Blob([JSON.stringify(_data,null,'\t')],{type:'application\/json'});
                const _url = URL.createObjectURL(_blob);
                const _link = document.createElement("a");
                _link.href = _url;
                _link.download = `${_petitId}_${_name}_petit_episode.json`;
                document.body.appendChild(_link);
                _link.click();
                document.body.removeChild(_link);
                URL.revokeObjectURL(_url);
            }
        });
    })();
}
+ ぷちエピソードURL + `https://(略)mobamas.net%2Fidolmaster%2Fsmart_phone_flash%2Fconvert%2Fpetit_cgSsSspetit_show_episode_swfSsSs[ぷちデレラアイドルID]SsSs[エピソード話数]SsSscoordinate_idolSsSs1%2F0%2F0%2F0%2F1%2F[ボイスあり1なし0]%2F0%2F0%2F1%3F` + アカウントの解放実績に紐づけられているっぽく、フォーマットに則ったURLでエピソードにアクセスしてもエラる模様

小ネタメモ / ajax API

プチページ上のソースを $.ajax({ とかで検索

テクニカルボード上にて window.path を Console 実行でも、なんかAPIぽいの?見られました

  • ぷちプロフ
    • %2Fpetit_cg%2Fajax_try_on_by_idol_id%3F
    • %2Fpetit_cg%2Freport_ajax_error%3F
    • %2Fpetit_cg%2Fajax_coordinate_list_by_idol_id%3F
    • %2Fpetit_cg%2Fajax_coordinate_by_idol_id%3F
    • %2Fpetit_cg%2Fajax_hair_styling_list_by_idol_id%3F
    • %2Fpetit_cg%2Fajax_hair_styling_by_idol_id%3F
    • %2Fpetit_cg%2Fajax_episode_list%3F
    • %2Fpetit_cg%2Fajax_get_show_unit_data%3F
    • %2Fpetit_cg%2Fajax_edit_unit_name%3F
    • %2Fpetit_cg%2Fajax_edit_background%3F
  • ぷち衣装リスト
    • %2Fpetit_cg%2Fajax_sell_accessory%3F 売却?
    • %2Fpetit_cg%2Fajax_lock_accessory%3F 保護?
    • %2Fpetit_cg%2Fajax_unlock_accessory%3F 保護解除?
    • %2Fpetit_cg%2Fajax_get_show_unit_data%3F ユニット情報取得?
    • %2Fpetit_cg%2Fajax_edit_unit_name%3F ユニット名編集? (ぷちめにゅー → ユニット名変更)
    • %2Fpetit_cg%2Fajax_edit_background%3F 背景変更? (ぷちめにゅー → 背景変更)

小ネタメモ / テクニカルボード

Console上で window.data 実行
window.data.idolId ぷちデレラアイドルID
window.data.idolIdCenter 現在のぷちユニットセンター
window.data.idolIdLeft 現在のぷちユニットレフト
window.data.idolIdRight 現在のぷちユニットライト
他、ボードに関する情報などが盛りだくさん。
プレイヤー固有の ぷちデレラお迎え時期が記載されている可能性アリ (あるいは実装日?)


知り合いの方・フォロワーさんで「実行したけどどうもうまくいかんかったわ」みたいなトコあったら、
何らかの形で教えてください (雑な意思表示)

他適当に随時更新するかもしれません。