Category Archives: dev

dev (category auto created by Wordpresser)

A solution to DL all YouTube videos, with proxy support

I wrote a very simple solution to help people generate Youtube-DL calls, to get all videos from a user, from a channel, or just from a single URL.

The solution is available at http://arturmarques.com/forms.pub/ytdl_cmd_gen/

It is nothing more than a learning device. It is built using plain HTML and plain JavaScript, with no frameworks or extensions necessary at all.
The code is written in the simplest possible fashion, using a pedagogical approach, with long and meaningful identifiers.

The operation is very simple: the user inputs a Youtube user name, or a Youtube channel name, or a Youtube video URL, and the page is expected to output a command-line command that enables any computer capable of running https://github.com/ytdl-org/youtube-dl to download the corresponding video(s), with the best available quality.

Some neat features are the support to proxies and a memory of past downloads. If you want to download via a proxy, even with authentication, the page should handle that.

The full source code is available from this post. This is just for learning. If something breaks and the solution stops working, you should be able to fix it yourself.

<!DOCTYPE html>
<!--
File: "ytdl_cmd_gen_pub.html"
Artur Marques, https://arturmarques.com/, 2019
 -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Youtube-DL Command Generator Helper</title>
    <script src="./am_minimal_pattern_v2019.js"></script>
    <script src="./YTDL_CMD_GEN_PUB_SECRET_DEFAULTS.js"></script>
    <script src="./ytdl_cmd_gen_pub.js"></script>
</head>
<body>
    <h1><a href="https://github.com/ytdl-org/youtube-dl">Youtube-DL</a> command gen to DL <mark>ALL</mark> video(s)...</h1>
    <h2>...from a channel OR from a user OR from a single URL</h2>

    <form id="idFormDownloadAllVideos">
        <fieldset>
            <legend>Input <strong>channel</strong> name <mark>OR</mark> <strong>user</strong> name <mark>OR</mark> single video URL:</legend>

            <label for="idSelectIdentifierType">This is the </label>
            <select id="idSelectIdentifierType">
                <option value="c" selected>channel</option>
                <option value="u">user</option>
                <option value="s">single video URL</option>
            </select>

            <label for="idTextChannelOrUserNameOrVideoUrl"> identifier: </label>
            <input
                    value="UCUa0DzKskGo0iRYP8QzWsvA"
                    size="60"
                    type="text"
                    id="idTextChannelOrUserNameOrVideoUrl"
                    placeholder="channel OR user identifier OR single video URL">
            <br>

            <label for="idCheckUseProxy">Use a proxy? </label>
            <input
                type="checkbox"
                id="idCheckUseProxy"
                checked>

            <details open id="idDetailsProxy">
                <summary>Proxy details</summary>
                    <fieldset>
                        <legend>Proxy URL address, port and (optionally) user/login and password</legend>
                        <label for="idTextUrlProxyAddress">Proxy URL address: </label>
                        <input
                                size="60"
                                type="text"
                                id="idTextUrlProxyAddress"
                                placeholder="e.g. someregion.somevpn.com">
                        <br>

                        <label for="idNumberProxyPort">Proxy port: </label>
                        <input
                                type="number"
                                id="idNumberProxyPort"
                                placeholder="proxy port here (e.g. 80)"
                                value="80">
                        <br>

                        <label for="idCheckLoginRequired">Login required? </label>
                        <input
                                type="checkbox"
                                id="idCheckLoginRequired"
                                checked>
                        <br>

                        <details open id="idDetailsProxyUserAndPass">
                            <summary>Proxy login and password</summary>
                                <fieldset>
                                    <legend>User/login name and password</legend>
                                    <label for="idTextProxyUser">User/login name: </label>
                                    <input
                                        type="text"
                                        id="idTextProxyUser"
                                        placeholder="username">
                                    <br>

                                    <label for="idPasswordProxyPass">Password: </label>
                                    <input
                                            type="password"
                                            id="idPasswordProxyPass"
                                            placeholder="">
                                    <br>
                                </fieldset>
                        </details>
                    </fieldset>
            </details>
            <br>

            <label for="idCheckKeepMemoryOfPreviousDls">Keep memory of previous downloads (<code>--download-archive</code> option)? </label>
            <input
                    type="checkbox"
                    id="idCheckKeepMemoryOfPreviousDls"
                    checked>
            <br>

            <label for="idTextYoutubedlOptions">Youtube-dl options (do NOT use <code>--download-archive</code> if set above): </label><br>
            <input
                size="80"
                type="text"
                id="idTextYoutubedlOptions"
                placeholder="youtube-dl options"
            >
            <br>

            <input
                id="idSubmitFormDownloadAllVideos"
                type="submit"
                value="get the command to download">
        </fieldset>
    </form>
    <hr>

<h2><label for="idTaCommand">Your <mark>command<sup>*</sup></mark> will appear here:</label></h2>
    <br>
    <textarea
        cols="80"
        rows="4"
        id="idTaCommand"></textarea>
    <p><sup>*</sup>The command is intended to be called from the command-line,
        from the directory where you installed <a href="https://github.com/ytdl-org/youtube-dl">Youtube-DL</a>
        or from any other, provided your system's PATH knows where Youtube-DL is installed.<br>
        You can resize the textarea from its lower-right corner.
    </p>
</body>
</html>

/*
File: ytdl_cmd_gen_pub.js
Artur Marques, https://arturmarques.com/, 2019
 */
window.onload = boot;

const PANIC = "One or more relevant object(s) are null. Will NOT proceed.";

const YOUTUBE_CHANNEL_PREFIX = "https://www.youtube.com/channel/";
const YOUTUBE_USER_PREFIX = "https://www.youtube.com/user/";
const PROTOCOL_STRING_END_MARK = "://";
const SUB_FOLDER_FOR_CHANNELS = "c";
const SUB_FOLDER_FOR_USERS = "u";

const
    ID_FORM_DOWNLOAD_ALL_VIDEOS = "idFormDownloadAllVideos",
    ID_SELECT_IDENTIFIER_TYPE = "idSelectIdentifierType",
    ID_TEXT_CHANNEL_OR_USER_NAME_OR_SINGLE_VIDEO_URL = "idTextChannelOrUserNameOrVideoUrl",

    ID_CHECK_USER_PROXY = "idCheckUseProxy",
    ID_DETAILS_PROXY = "idDetailsProxy",
    ID_TEXT_URL_PROXY_ADDRESS = "idTextUrlProxyAddress",
    ID_NUMBER_PROXY_PORT = "idNumberProxyPort",
    ID_CHECK_LOGIN_REQUIRED = "idCheckLoginRequired",
    ID_DETAILS_PROXY_USER_AND_PASS = "idDetailsProxyUserAndPass",
    ID_TEXT_PROXY_USER = "idTextProxyUser",
    ID_PASSWORD_PROXY_PASS = "idPasswordProxyPass",

    ID_CHECK_KEEP_MEMORY_OF_PREVIOUS_DLS = "idCheckKeepMemoryOfPreviousDls",
    ID_TEXT_YOUTUBEDL_OPTIONS = "idTextYoutubedlOptions",
    ID_SUBMIT_FORM_DOWNLOAD_ALL_VIDEOS = "idSubmitFormDownloadAllVideos",
    ID_TA_COMMAND = "idTaCommand";

let oFormDownloadAllVideos, oSelectIdentifierType, oTextChannelOrUserNameOrVideoUrl,
    oCheckUseProxy, oDetailsProxy, oTextUrlProxyAddress, oNumberProxyPort, oCheckLoginRequired,
    oDetailsProxyUserAndPass, oTextProxyUser, oPasswordProxyPass,
    oCheckKeepMemoryOfPreviousDls,
    oTextYoutubedlOptions,
    oSubmitFormDownloadAllVideos,
    oTaCommand;

function boot(){
    oFormDownloadAllVideos = $(ID_FORM_DOWNLOAD_ALL_VIDEOS);
    oSelectIdentifierType = $(ID_SELECT_IDENTIFIER_TYPE);
    oTextChannelOrUserNameOrVideoUrl = $(ID_TEXT_CHANNEL_OR_USER_NAME_OR_SINGLE_VIDEO_URL);
    oCheckUseProxy = $(ID_CHECK_USER_PROXY);
    oDetailsProxy = $(ID_DETAILS_PROXY);
    oTextUrlProxyAddress = $(ID_TEXT_URL_PROXY_ADDRESS);
    oNumberProxyPort = $(ID_NUMBER_PROXY_PORT);
    oCheckLoginRequired = $(ID_CHECK_LOGIN_REQUIRED);
    oDetailsProxyUserAndPass = $(ID_DETAILS_PROXY_USER_AND_PASS);
    oTextProxyUser = $(ID_TEXT_PROXY_USER);
    oPasswordProxyPass= $(ID_PASSWORD_PROXY_PASS);
    oCheckKeepMemoryOfPreviousDls = $(ID_CHECK_KEEP_MEMORY_OF_PREVIOUS_DLS);
    oTextYoutubedlOptions = $(ID_TEXT_YOUTUBEDL_OPTIONS);
    oSubmitFormDownloadAllVideos = $(ID_SUBMIT_FORM_DOWNLOAD_ALL_VIDEOS);
    oTaCommand = $(ID_TA_COMMAND);

    let aAllRelevant = [
        oFormDownloadAllVideos, oSelectIdentifierType, oTextChannelOrUserNameOrVideoUrl,
        oCheckUseProxy, oDetailsProxy, oTextUrlProxyAddress, oNumberProxyPort, oCheckLoginRequired,
        oDetailsProxyUserAndPass, oTextProxyUser, oPasswordProxyPass,
        oCheckKeepMemoryOfPreviousDls,
        oTextYoutubedlOptions,
        oSubmitFormDownloadAllVideos,
        oTaCommand
    ];

    let bAllOK = allNotNull(aAllRelevant);

    if (bAllOK){
        setDefaultsFromSecrets();

        oTextChannelOrUserNameOrVideoUrl.oninput =
        oTextYoutubedlOptions.oninput =
        oSelectIdentifierType.onchange = actionUpdateToReflectCurrentUserInputs;

        oCheckUseProxy.onchange = function() { oDetailsProxy.open = oCheckUseProxy.checked; actionUpdateToReflectCurrentUserInputs(); };
        oCheckLoginRequired.onchange = function() { oDetailsProxyUserAndPass.open = oCheckLoginRequired.checked; actionUpdateToReflectCurrentUserInputs(); };
        oCheckKeepMemoryOfPreviousDls.onchange = function() { actionUpdateToReflectCurrentUserInputs(); };
        oNumberProxyPort.onchange = function() { actionUpdateToReflectCurrentUserInputs(); };

        oFormDownloadAllVideos.onsubmit = actionDownloadAllVideos;

        actionUpdateToReflectCurrentUserInputs();
    }//if
    else{
        window.console.log(PANIC);
        alert (PANIC);
        return;
    }//else
}//boot

function actionUpdateToReflectCurrentUserInputs (
    pTheEvent
){
    oTaCommand.value = computeYoutubedlCommandFromCurrentFormData();
}//actionUpdateToReflectCurrentUserInputs

/*
implies the availability of file "YTDL_CMD_GEN_PUB_SECRET_DEFAULTS.js"
loaded from HTML
defining the following consts:
const DEFAULT_CHECK_USE_PROXY = some boolean;
const DEFAULT_TEXT_URL_PROXY_ADDRESS = "?";
const DEFAULT_NUMBER_PROXY_PORT = some number;
const DEFAULT_CHECK_LOGIN_REQUIRED = some boolean;
const DEFAULT_TEXT_PROXY_USER = "?";
const DEFAULT_PASSWORD_PROXY_PASS = "?";
 */
function setDefaultsFromSecrets(){
    oTextYoutubedlOptions.value = DEFAULT_TEXT_YOUTUBEDL_OPTIONS;

    oCheckUseProxy.checked = DEFAULT_CHECK_USE_PROXY;
    if (oCheckUseProxy.checked){
        oDetailsProxy.open = true;
        oTextUrlProxyAddress.value = DEFAULT_TEXT_URL_PROXY_ADDRESS;
        oNumberProxyPort.value = DEFAULT_NUMBER_PROXY_PORT;

        oCheckLoginRequired.checked = DEFAULT_CHECK_LOGIN_REQUIRED;
        if (oCheckLoginRequired.checked){
            oDetailsProxyUserAndPass.open = true;
            oTextProxyUser.value = DEFAULT_TEXT_PROXY_USER;
            oPasswordProxyPass.value = DEFAULT_PASSWORD_PROXY_PASS;
        }//if
        else{
            oDetailsProxyUserAndPass.open = false;
            oTextProxyUser.value = "";
            oPasswordProxyPass.value = "";
        }
    }//if
    else{
        oDetailsProxy.open = false;
        oTextUrlProxyAddress.value = "";
        oNumberProxyPort.value = "";
    }//else
}//setDefaultsFromSecrets

function computeYoutubedlCommandFromCurrentFormData(){
    let bTypeChannel = oSelectIdentifierType.value==="c" || oSelectIdentifierType.value==="C";
    let bTypeUser = oSelectIdentifierType.value==="u" || oSelectIdentifierType.value==="U";
    let bSingleVideoUrl = oSelectIdentifierType.value==="s" || oSelectIdentifierType.value==="s";

    let strCMD ="youtube-dl ";
    if (oCheckUseProxy.checked){
        strCMD+=" --proxy ";
        if (oCheckLoginRequired.checked){
            //proxy with authentication
            let nProtocolMarkEndPos = oTextUrlProxyAddress.value.indexOf(PROTOCOL_STRING_END_MARK) + PROTOCOL_STRING_END_MARK.length;
            let strProxyUrlWithoutProtocol = oTextUrlProxyAddress.value.substr(nProtocolMarkEndPos);
            let strProtocolIncludingEndMark = oTextUrlProxyAddress.value.substr(0, nProtocolMarkEndPos);
            let strUrlWithAuthentication =
                strProtocolIncludingEndMark +
                oTextProxyUser.value+
                ":"+oPasswordProxyPass.value+"@"+
                strProxyUrlWithoutProtocol+":"+
                oNumberProxyPort.value+" ";
            strCMD += strUrlWithAuthentication;
        }//if
        else{
            //no proxy
            strCMD += oTextUrlProxyAddress.value+":"+oNumberProxyPort.value+" ";
        }//else
    }//if
    strCMD+=oTextYoutubedlOptions.value+" ";

    if (oCheckKeepMemoryOfPreviousDls.checked){
        strCMD+=" --download-archive "+"DB_"+oTextChannelOrUserNameOrVideoUrl.value+".TXT ";
    }//if

    if (bTypeUser){
        //e.g. end with ytuser:bostondynamics
        strCMD += "ytuser:"+oTextChannelOrUserNameOrVideoUrl.value;
    }//if

    if (bTypeChannel){
        //e.g. UCUa0DzKskGo0iRYP8QzWsvA => end with full channel URL
        strCMD += YOUTUBE_CHANNEL_PREFIX+oTextChannelOrUserNameOrVideoUrl.value;
    }//if

    if (bSingleVideoUrl){
        strCMD += oTextChannelOrUserNameOrVideoUrl.value;
    }
    return strCMD;
}//computeYoutubedlCommandFromCurrentFormData

function actionDownloadAllVideos(
    pTheEvent
){
    let bEmptyChannelOrUserName = oTextChannelOrUserNameOrVideoUrl.value.trim() === "";
    if (!bEmptyChannelOrUserName){
        let strFullCMD = computeYoutubedlCommandFromCurrentFormData();
        oTaCommand.innerHTML = strFullCMD;
        alert ("Done. The command is at the \"command\" text area.")
    }//if
    else{
        alert ("Empty channel or user name or single video URL! Nothing done.")
    }
    return false;
}//actionDownloadAllVideos

/*
File: am_minimal_pattern_v2019.js
Artur Marques, https://arturmarques.com/, 2019
 */
//--------------------------------------------------------------------------------
function $ (pId){return document.getElementById(pId);}

//--------------------------------------------------------------------------------
function allNotNull (pObjects){
    let bAllNotNull = true;
    //for (var o of pObjects){
    for (let idx=0; idx<pObjects.length; idx++)
    {
        let o = pObjects[idx]; //comentar esta linha se usarmos for..of
        if (o===null){
            let strMsg = "element @idx "+idx+" is null\n";
            console.log (strMsg);
        }
        bAllNotNull = bAllNotNull && (o!==null);
    }//for
    return bAllNotNull;
}//allNotNull


/*
File: YTDL_CMD_GEN_PUB_SECRET_DEFAULTS.js
*/
const DEFAULT_CHECK_USE_PROXY = true;
const DEFAULT_TEXT_URL_PROXY_ADDRESS = "https://someregion.somevpn.com"; //do NOT terminate with /
const DEFAULT_NUMBER_PROXY_PORT = 80;
const DEFAULT_CHECK_LOGIN_REQUIRED = true;
const DEFAULT_TEXT_PROXY_USER = "\"your@emailForExample.com\"";
const DEFAULT_PASSWORD_PROXY_PASS = "yourPassword";
const DEFAULT_TEXT_YOUTUBEDL_OPTIONS = " -f best -ciw --write-description --write-annotations --write-thumbnail ";

PHPStorm fails refactors in symlinked files

Yesterday, while programming a solution that will automatically build some of my videos, I faced an unexpected problem with JetBrains PHPStorm v19.2.

The issue was (and is) that doing a “refactor > rename” of any method’s parameter would not work. The IDE’s “preview” would correctly show the changes that should happen, but upon confirmation, by pressing the ENTER key, while the parameter name would change, its usages would not, so the code would not work, because of the new unknown identifiers.

This would only happen with identifiers corresponding to method parameters, which are “signature” elements, together with the method’s name and its return type.
After some head scratching, I understood what is the issue: the file corresponding to the class of the method being edited, is available to the project not in a “real” directory, but via a “symbolic link”, which a Microsoft Windows file system’s feature that allows, among other possibilities, to make the same single folder seemingly available at many different points. It happens that PHPStorm will NOT change signatures for methods in files, which are not directly available to projects. If the file is available to the project as a real direct, then “refactor > rename” works; if the file is in a symlink, it does NOT.

Problem understood. This is annoying, but now understandable.

I cannot say for sure that is new in the latest version of PHPStorm (v19.2, in August 2019), but I don’t remember having faced the issue in the past, and I’ve used symlinks before.

I tweeted about the mysterious issue here:
https://twitter.com/my_dot_com/status/1163544932138790916

“@JetBrains I think you created a bug in PHPStorm 19.2
Try to refactor > rename any param in a method’s param list. You will see a CORRECT preview in the body, but when ENTER is pressed, the param is renamed yet the body keeps the old identifiers.”

Then, when I understood the problem, I replied to myself here:
https://twitter.com/my_dot_com/status/1163764619057467393

“Replying to my own problem: I found the issue is due to symbolic links (file system symlinks) in Windows. The project in question uses a folder which is a symlink to a real directory. Refactor of signature elements (such as params) in files at that location will fail.”

File system organization automation – moving thousands of files

I am trying to organize large thousands of files.
The task is not healthy if done by hand; moreover, it is prone to human error. So, I wrote an automated solution, using PHP and some of its file system calls.

The problem is described in the main function comments, here shared in this post. Imagine thousands of folders, each eventually containing a single specially named sub-folder (e.g. “_”) whose contents (files and directories) you want to move to its parent root. This was interesting to write because it shows the power of a language’s file system tools for automation in files organization.

Check the “before” and “after” images for the simplest example I managed to capture in a visual representation.

/*
 * 2019-07-04 started
 * 2019-07-04 first tested ok - test preserved in file "test_20190704.php"
 * learned: https://www.php.net/manual/en/class.splfileinfo.php
 * the idea is to move the contents in <some folder>\_\<contents> to <some folder>\<contents>
 * the "_" is the product of a rename "shortener" tool - it facilitates in avoiding very long paths that result from some jobs
 * however, some archives when unpacked will still have sub-folders inside the _ folder
 * and that organization creates problems to other tools of mine
 * this function should received a folder F path, of a folder which might contain a "_" named sub folder
 * then it will move _'s files and folders to the F's root
 *
 * @param string $pStrFolderPath : some folder path
 * @param string $pStrSpecificNameOfSubFolderWhoseContentsAreToBeMovedToParentFolder : optional, the name of the special folder, defaults to "_"
 * @param bool $pbFeedback : verbose activity, giving feedback to user? Defaults to true
 * @param bool $pbCaseSensitive : case sensitive in checking the folder? Default to false and is not relevant for folders with names like "_"
 * @return int : the number of successful ren/move operations
 */
function moveEverythingInFolderWithSpecificNameIfItExistsInPathToItsParentFolder (
    string $pStrFolderPath,
    string $pStrSpecificNameOfSubFolderWhoseContentsAreToBeMovedToParentFolder = "_",
    $pbFeedback=true,
    $pbCaseSensitive = false
)
{
    $bPathExists=file_exists($pStrFolderPath);
    $iSuccessfulRenamesCounter=$iFailedRenames=0;

    if ($pbFeedback){
        feedbackOnFunctionCall(
            __FUNCTION__,
            func_get_args()
        );
    }//if

    if ($bPathExists) {
        //dirsInDir does NOT return the dots dirs . and ..
        $dirsInDir = dirsInDir(
            $pStrFolderPath,
            false //not recursive
        );
        $iHowManyDirsInDir = count($dirsInDir);
        $bRightNumberOfDirs = ($iHowManyDirsInDir === 1); // $pStrSpecificNameOfSubFolderWhoseContentsAreToBeMovedToParentFolder

        //there should be only 1 dir if the move op is to be safe in preserving the contents' hierarchy
        if ($bRightNumberOfDirs) {
            $oSingleDirItemObject = $dirsInDir[0];
            $strDirName = $oSingleDirItemObject["fname"];

            $bFolderHasTheSearchedForName =
                $pbCaseSensitive ?
                    //sensitive
                    strcmp(
                        $strDirName,
                        $pStrSpecificNameOfSubFolderWhoseContentsAreToBeMovedToParentFolder
                    ) === 0

                    :
                    //insensitive
                    strcasecmp(
                        $strDirName,
                        $pStrSpecificNameOfSubFolderWhoseContentsAreToBeMovedToParentFolder
                    ) === 0;

            $oParentFolderPath = new SplFileInfo ($pStrFolderPath);
            $strParentDirectoryFullPath = $oParentFolderPath->getRealPath();

            if ($bFolderHasTheSearchedForName) {
                $strOldLocationOfSearchedFolder = $oSingleDirItemObject["realPath"];

                //dirItems is a function of mine, returning an assoc array representing each dir in a path, except the dot dirs (. and ..)
                $aItemsInSearchedFolder = dirItems(
                    $strOldLocationOfSearchedFolder,
                    "*",
                    false //not recursive
                );

                foreach ($aItemsInSearchedFolder as $itemAsAssocArray) {
                    $strOldLocationOfSearchedFolder = $itemAsAssocArray["realPath"];
                    $strItemName = $itemAsAssocArray["fname"];
                    $strNewLocationForSearchedFolderContents = $strParentDirectoryFullPath . "\\" . $strItemName;

                    $bRenameMoveItemOK = rename(
                        $strOldLocationOfSearchedFolder,
                        $strNewLocationForSearchedFolderContents
                    );
                    if ($bRenameMoveItemOK) {
                        $iSuccessfulRenamesCounter++;
                    }//if ok
                    else {
                        echo "Failed in moving $strOldLocationOfSearchedFolder to $strNewLocationForSearchedFolderContents" . PHP_EOL;
                        $iFailedRenames++;
                    }//else
                }//foreach item in folder

                $strFullPathOfSearchedFolder = $oSingleDirItemObject['realPath'];
                $aItemsInSearchedFolder = dirItems($strFullPathOfSearchedFolder);
                $bEmptyFolder = count($aItemsInSearchedFolder)===0;
                if ($bEmptyFolder){
                    $rmResult = rmdir($strFullPathOfSearchedFolder); //return not used, but it is a bool
                }//if
            }//if found the searched folder
        }//if there is exactly 1 folder in the path
    }//if path exists

    return $iSuccessfulRenamesCounter;
}//moveEverythingInFolderWithSpecificNameIfItExistsInPathToItsParentFolder



mover_after.png
https://arturmarques.com/wp/wp-content/uploads/2019/07/mover_after.png (image/png)

mover_after.png


mover_before.png
https://arturmarques.com/wp/wp-content/uploads/2019/07/mover_before.png (image/png)

mover_before.png

Technical Details

A quick solution to check if a string ends with…

Today I was coding a PHP solution to remove tracking data from URLs. To help in doing so, my approach to the problem requires being able to check if a string ends in another. PHP doesn’t seem to have a prebuilt “endsWith” function.
So, I wrote an auxiliary tool to do just that (check if a string ends in another), dumped it as static method to an “Util” class of mine (being static, no need to instantiate the class to use it), and gave it the bonus of being able to do the checking in a case sensitive fashion, or not.

    //_.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-^-._.-
    /*
     * 2019-07-03
     * @param string $pStr : a string
     * @param string $pStrTermination : a string to be tested as a termination of the first
     * @return bool : true if indeed $pStrTermination is at the end of $pStr
     */
    public static function auxCheckIfStringsEndsWith(
        string $pStr, //some string
        string $pStrTermination, //some string that eventually is at the end of $pStr
        bool $pbCaseSensitive = false //by default, don't be case sensitive in checking
    ){
        $iLengthOfString = strlen($pStr);
        $iLengthOfTermination = strlen($pStrTermination);
        $bItCanBeATermination = $iLengthOfString>=$iLengthOfTermination;

        if ($bItCanBeATermination){
            $iPosWhereEventualTerminationStarts =
                $pbCaseSensitive ?
                strpos($pStr, $pStrTermination) //case sensitive checking
                :
                stripos($pStr, $pStrTermination); //case insensitive checking

            if ($iPosWhereEventualTerminationStarts!==false){
                //termination exists, but is it at the exact end of string?
                $bAtTheExactEnd = $iPosWhereEventualTerminationStarts === $iLengthOfString - $iLengthOfTermination;
                return $bAtTheExactEnd;
            }//if
        }//if
        return false;
    }//auxCheckIfStringsEndsWith