広告の品「CFD販売 CSSD-S6B480CG3VX」を購入したらハズレを引いた件。。。

f:id:dot-harley:20200309162454j:plain:alt=アイキャッチ

いままでマイドキュメントやマイピクチャなどのユーザーデータを保存しておくドライブとして使用していた「Hitachi HDP725050GLA360」。1年ほど前から気づいていたのですが、SMART で「3個の不良セクターがありますが、使用可能です」と診断されています。

旧 HDD

「使用可能です」とはいうものの、精神衛生上よろしくない。

すでに 10年ぐらい使ってるし、そろそろ買い替えどきかな?Hitachi HDP725050GLA360 くん、よく頑張りました。

このさい SSD に換装しようか。

しばらく価格比較サイトを閲覧しながら検討していると、ある日突然。。。

広告の品発見!

CFD販売 CSSD-S6B480CG3VX が ピーシーデポにて数量限定 ¥6000也。価格.com の注目ランキング、売れ筋ランキングともに第1位の品です。

NTT-X Store で CFD販売 CSSD-S6B480CG3VX の現在の価格をチェック!
CFD CSSD-S6B480CG3VX

喜び勇んで買いに行ったものの、税込みだと ¥6600。価格.com での最安値が送料税込み ¥6980 なので、そこまで「安い!」ってほどじゃありませんでしたね。

とはいえ、店頭販売は初期不良対応に強みがあります。

ネット通販で購入した商品に万が一ですが初期不良があった場合。メールか電話で問い合わせをして、商品を返送することになります。

はい、返送料がかかりますね。せっかく最安値を探して購入したのに、かえって割高になってしまいます。

いっぽう店頭販売なら「初期不良です」といって販売店へ持ち込めばよろしい。メールや電話より話も通じやすいです。

などと考えつつ購入するも直後に多忙な日々が続き、気がつけば 1ヶ月ほど放置。。。

ようやくまとまった時間が出来た頃、CSSD-S6B480CG3VX の存在を思い出す。

データを移行する前に動作チェックだな、と Linux Mint のディスクユーティリティで速度を計測。

ディスクユーティリティで速度計測

なんじゃこりあぁぁぁ!!!

読み書きともに公称値の半分程度しか出てないじゃないか!

。。。

まあ待て。落ち着け。

Linux がおかしいのかもしれん。Windows で計測してみようじゃないか、と CrystalDiskMark で速度計測。

CrystalDiskMark で速度計測

ダメです!

ショックのあまりヨシダはヨシダみたいな言い方になってしまいました。←まだ混乱している🤯

この時点で初期不良を疑いつつ、Windows 付属のエラーチェックを実行。

Windows のエラーチェック

え!?正常。。。だ、と??

どーなってるんだーーー!!!

ひととおり錯乱したところで、気を取り直して FromHDDtoSSD でエラーチェック。

FromHDDtoSSD でエラーチェック

FromHDDtoSSD の完全スキャンレポート

やっぱり出た!不良セクタ

新品未使用なのに「劣化し始めてる」とか言われてるし。。。

それにディスク容量に表示されている値もおかしいし。

うん、初期不良ですね(確信)。

さっそくお店に持っていって初期不良であることを告げる。

しばらく待たされたあと、速度が出ないと確認できたので初期不良対応します、とのこと。

ただし「現在在庫がないため取り寄せになります。何日かかるかは分かりません。返金にしますか?」という。

う〜む。どうしよう。

このときすでにメーカーにも販売店にもネガティブなイメージがびっしりついていました。この場で返金してもらって他のメーカー製の SSD を買おう。CFD も ピーシーデポも二度と使うもんか!という考えに傾いていました。

しばし悩んだ末、けっきょく取り寄せで交換してもらうことにしました。いまからネット通販しても手元に届くまで同じぐらいの日数かかるでしょう。せっかくセール価格で手に入れたのに返金はもったいないし。

というわけで、ただいま入荷待ちの日々。

VLC media player のプレイリストファイルから動画に埋め込むチャプターファイルを生成する CopyQ スクリプト

f:id:dot-harley:20200110233155j:plain

以前 VLC media player のカスタムブックマークを利用してお気に入りのシーンを保存する方法を記事にしました。

VLC media player のカスタムブックマークを利用する - In my mind

ですが、これだと動画ファイルとともにプレイリストファイルも管理しなければなりません。動画のファイル名を変更したらプレイリストファイルの中身も書き換えなきゃいけないとか面倒です。

やっぱり動画にチャプターを埋め込みたい。

そこで VLC media player のプレイリストファイルからチャプターファイルを生成する CopyQ スクリプトを書いてみました。

特徴

  • VLC media player のプレイリストファイルから動画に埋め込むチャプターファイルを生成します。

  • プレイリストに複数の動画が登録されている場合、カスタムブックマークが登録されている動画すべてのチャプターファイルを生成します。

  • 生成されるチャプターファイルは Nero 形式。mkv や mp4 に埋め込むことが出来ます。

  • 童画の初め 00:00:00.000 時点にカスタムブックマークが登録されていない場合は自動でチャプターが追加されます。

  • 「もっとスマートなやり方があるよ」という方はぜひコメント欄にお願いします。

免責事項

無保証です。このスクリプトを使用して何らかの損害が発生したとしても、作者は責任を負いません。

不具合の報告はコメント欄にお願いします。直せるかどうか分かりませんが。。。

コード

[Command]
Automatic=true
Command="
    copyq:
    // \x79d2\x3092\x6642\xff1a\x5206\xff1a\x79d2\x306b\x5909\x63db\x3059\x308b\x95a2\x6570
    function toHms(t) {
      var hms = \"\";
      var h = t / 3600 | 0;
      var m = t % 3600 / 60 | 0;
      var s = t % 60;
    
      hms = padZero(h) + \":\" + padZero(m) + \":\" + padZero(s);
    
      return hms;
    
      function padZero(v) {
        return (\"0\" + v).slice(-2)
      }
    }
    
    // xspf \x30d5\x30a1\x30a4\x30eb\x306e\x30d1\x30b9\x3092\x53d6\x5f97
    var xspfLocation = str(clipboard('text/plain'))
    
    // xspf \x30d5\x30a1\x30a4\x30eb\x306e\x5185\x5bb9\x3092\x8aad\x307f\x3053\x3080
    var f = new File (xspfLocation)
    if ( ! f . openReadOnly ())
      throw 'Failed to open the file: ' + f . errorString ()
    var bytes = f . readAll ();
    if( ! str(bytes).match(/xmlns:vlc=\\\"http:\\/\\/www\\.videolan\\.org\\/vlc\\/playlist/)){
      popup('This is NOT VLC playlist file!')
    }
    
    // <track> \x3092\x914d\x5217\x306b\x683c\x7d0d
    var tracks = str(bytes).match( /<track>[\\s\\S]+?<\\/track>/g );
    
    for (r in tracks){
      if(! tracks[r].match(/<vlc:option>bookmarks={/ ))
        continue
      // \x51fa\x529b\x5148
      var location = tracks[r].replace(/^[\\s\\S]*?<location>file:\\/\\/(.+?)<\\/location>[\\s\\S]*$/ , '$1.chapter.txt')
      // \x30ab\x30b9\x30bf\x30e0\x30d6\x30c3\x30af\x30de\x30fc\x30af\x3092\x53d6\x5f97
      var bookmarks = tracks[r].match(/\\{name=.+?,time=.+?\\}/g );
      var bookmarksArry = [];
      for(s in bookmarks){
        bookmarksArry[s] = {}
        bookmarksArry[s].name = bookmarks[s].replace(/^.*?name=(.+?),.*$/ , '$1')
        bookmarksArry[s].time = bookmarks[s].replace(/^.*?time=(\\d+?)\\.\\d*\\}/ , '$1')
        bookmarksArry[s].frame = bookmarks[s].replace(/^.*?time=\\d+?\\.(\\d*)\\}/ , '$1')
      }  
      // \x30ab\x30b9\x30bf\x30e0\x30d6\x30c3\x30af\x30de\x30fc\x30af\x3092\x6642\x9593\x9806\x306b\x30bd\x30fc\x30c8
      function compareFunc(a, b){
        return a.time - b.time;
      }
      bookmarksArry.sort(compareFunc);
      // \x30c1\x30e3\x30d7\x30bf\x30fc\x30d5\x30a1\x30a4\x30eb\x306e\x5185\x5bb9\x3092\x751f\x6210
      var chapter = ''
      var m = 0
      if(bookmarksArry[0].time != 0 && bookmarksArry[0].frame != 0){
        chapter = 'CHAPTER01=00:00:00.000\\nCHAPTER01NAME=Start\\n'
        m = 1
      }
      for(s in bookmarksArry){
        n = Number(s) + 1 + m;
        chapter += 'CHAPTER' + ('0' + n).slice(-2) + '=' + toHms(bookmarksArry[s].time) + '.' + bookmarksArry[s].frame + '\\n'
          + 'CHAPTER' + ('0' + n).slice(-2) + 'NAME=' + bookmarksArry[s].name + '\\n'
      }
      add(chapter)
      //  \x30c1\x30e3\x30d7\x30bf\x30fc\x30d5\x30a1\x30a4\x30eb\x3092\x66f8\x304d\x51fa\x3059
      var g = new File ( decodeURIComponent(location) )
      if ( ! g . openWriteOnly () || g . write ( chapter ) == - 1 )
        throw 'Failed to save the file: ' + g . errorString ()
      // Always flush the data and close the file,
      // before opening the file in other application.
      g . close ()
    }"
Icon=\xf008
Input=text/plain
Match=\\.xspf$
Name=VLC playlist.xspf -> chapter.txt

使い方

準備

あらかじめ以下の記事を参考に、スクリプトを登録しておきます。

CopyQ に共有スクリプトを追加する方法 - In my mind

あらかじめ CopyQ を起動しておきます。

動画とそのプレイリストファイルを用意します。

今回は動画「Cat-2879.mp4」とプレイリストファイル「Cat-2879.xspf」を用意しました。

チャプターファイルを生成

プレイリストファイル「Cat-2879.xspf」をクリップボードにコピーするだけです。

プレイリストファイルをクリップボードにコピー

ほどなくチャプターファイルが生成されます。

ファイル名は「動画名 + .chapter.txt」となります。今回は「Cat-2879.mp4.chapter.txt」ですね。

チャプターファイルが生成された

動画にチャプターを埋め込む

今回は MkvToolnix を使って動画にチャプターを埋め込みます。

MkvToolnix で動画ファイルを開きます。

MkvToolnix で動画を開く

  1. 出力タブに移ります。

  2. さきほど生成されたチャプターファイルを選択。

  3. 出力先とファイル名を指定します。

  4. 「Start multiplexing」で即時出力するなり「ジョブキューに追加」して後からまとめて出力するなりお好きなように。

チャプターを埋め込む

確認

出来上がった動画ファイルを VLC media player で再生してみます。

メニュー > 再生 > チャプター から好きなシーンにジャンプできるようになりました。

チャプターが埋め込まれた

参考

MP4のチャプターファイルについて: Take it eazy!!


アイキャッチ画像素材提供:A MacBook with lines of code on its screen on a busy desk photo – Free Computer Image on Unsplash

記事の推敲に便利!「Intelligent Speaker」の使い方【Firefox/Chrome 機能拡張】

f:id:dot-harley:20200117231106j:plain

ブログの記事を推敲するさい、誰かに読み上げてもらうと捗りますよね。

しかし毎回毎回人に頼むことも出来ないので結局コンピューターの力を借りることになります。

かつては↓のブックマークレットを使っていました。

ウェブサイトのテキストをクリックで音読するブックマークレット - @kyanny's blog

Firefox のリーダービューから読み上げることも出来ますね。

Windows なら良いのですが、Linux ではうまくいきません。

英単語はちゃんと読み上げてくれるものの、ひらがな・カタカナ・漢字は1文字1文字「Japanese character」とか「Chinese character」としか読んでくれません。

Open JTalkでもインストールすれば日本語で喋ってくれるのかもしれません。しかし、サンプル音声を聞いた限りではノイズが多くて聞くに耐えません。

決定的な打開策もないまま約1年が過ぎ。。。

ようやく良いアドオンにめぐり逢いました!!!

「Intelligent Speaker」です。

公式サイト:Intelligent Speaker: text to speech browser extension, synced with podcast feed

↑のページから FirefoxChromeOpera それぞれの機能拡張ページへ飛べます。

インストールが済んだらさっそく使ってみましょう。

読み上げたいページを開きます。

Intelligent Speaker の機能拡張アイコンをクリックして「+」ボタンをクリック。

するとリストにページが追加されます。

ページを追加

一部のテキストだけ読み上げたい場合は選択範囲を右クリックして「フィードに追加」。

選択範囲を追加

リストに追加されたアイテム左側のアイコンをクリックすると音声が再生されます。

読み上げる

「設定」からボイスを選択できます。

Female: Mizuki

デフォルトは「Female: Mizuki」とかいう聞いたことがないボイス。

これがダンゼン聞き取りやすいのです。

調べてみるとどうやら「Male: Takumi」とともにAmazon Pollyなる有料音声合成サービスのボイスだという。

有料音声合成エンジンを使っているためか、Intelligent Speaker には使用制限があります。

無料枠は「1時間/月」となってます。

私の使い方では無料枠で間に合いそうですね。存分に活用させていただきますm(__)m

アイキャッチ画像素材提供: George W. Bush standing on lectern during daytime photo – Free Human Image on Unsplash

【解決済】 CopyQ で画像がコピーできない

f:id:dot-harley:20200116051929j:plain

クリップボード拡張ソフト「CopyQ」を常用しております。

もはやコレなしでは記事も書けないところまで来てます。

いくつか CopyQ スクリプト まで書いておいて今更ですが、重大な欠陥に気が付きました。

画像がコピーできないのです。

Web 上に表示された画像をコピーして GIMP で編集しようとしても開けません。

CopyQ を常駐していないときはうまく行くので、CopyQ のせいで間違いないでしょう。

原因を探るべく公式ドキュメントを読んでみる。。。

下の記事によると「CopyQ の設定 > アイテム」から画像関連の設定ができるとのことですが、私のは空っぽです。

Images — CopyQ documentation

CopyQ の設定 > アイテム

ナニカガオカシイ。。。

もしや CopyQ のバージョンが古いのか?たしか Flatpack 版もあったはずだ。インストールしてみようか、と「ソフトウェアの管理」を開いてみると。。。

「Copyq-plugins」がインストールされていないことに気が付きました(>ω・)てへぺろ

ソフトウェアの管理 > Copyq-plugins

早速インストールしたところ、「CopyQ の設定 > アイテム」で画像関連の設定ができるようになりました。

CopyQ の設定 > アイテム (プラグイン導入済)

コピーした画像も CopyQ に保存されています。

画像がコピーできた

いや〜、八兵衛もビックリのうっかりさんでしたね。おはずかしい。

ともあれ CopyQ で画像が保存できるようになったので、めでたしめでたし。どっとはれ。

アイキャッチ画像素材提供: woman holding scissor photo – Free Person Image on Unsplash

VLC media player のカスタムブックマークを利用する

f:id:dot-harley:20200116010259j:plain

VLC media player のカスタムブックマークの使い方を説明します。

DVD のチャプター機能のようにシーンをジャンプすることが出来ます。

DVD のチャプターと違って任意のシーンをブックマークすることができます。

では実際に使ってみましょう。

VLC media player で動画を再生します。

メニュー > 再生 > カスタムブックマーク > 管理 をクリックしてブックマークの管理ウィンドウを表示します。

管理ウィンドウを呼び出す

ブックマークしたいシーンが来たら「作成」ボタンをクリックします。

新しいブックマークを追加

ダブルクリックでブックマークしたシーンにジャンプできます。

ダブルクリックでジャンプ

説明欄には「ファイル名 番号」が自動で入力されます。

説明欄をゆっくり2回クリックすると編集できます。ダブルクリックと判定されないようにゆっくりですよ。

後で観るときに分かりやすい名前を付けてあげましょう。

説明を編集

作成されたカスタムブックマークは自動で保存されません

このまま VLC media player を閉じると消失してしまいます。

なのでカスタムブックマークを手動で保存します。

メニュー > メディア > プレイリストファイルの保存 をクリック。

任意のファイル名を付けて保存します。

プレイリストを保存

次回カスタムブックマークを保持して再生したいときは、動画ファイルではなくプレイリストファイルのほうを再生します。

以上、VLC media player のカスタムブックマークの使い方でした。

動画素材提供: 猫 ネコ科 ペット - Pixabayの無料動画

アイキャッチ画像素材提供: flat screen TV photo – Free Plant Image on Unsplash

これは便利!選択範囲内のテキストを1行1アイテムとしてコピーする CopyQ スクリプト

f:id:dot-harley:20200110233155j:plain

選択範囲内のテキストを改行で区切ってコピー、つまり1行1アイテムとして CopyQ に保存したい時ってありますよね。

それ CopyQ スクリプトで出来ます!

特徴

  • 1行1アイテムとして CopyQ に保存されます。

  • 空白行は無視されます。

  • 出力先のタブが選べます。定型文の登録に便利。

  • 「もっとスマートなやり方があるよ」という方はぜひコメント欄にお願いします。

免責事項

無保証です。このスクリプトを使用して何らかの損害が発生したとしても、作者は責任を負いません。

不具合の報告はコメント欄にお願いします。直せるかどうか分かりませんが。。。

コード

[Command]
Command="
    copyq:
    if (!copy())
      abort()
    
    // \x9078\x629e\x7bc4\x56f2\x306e\x30c6\x30ad\x30b9\x30c8\x3092\x5909\x6570\x306b\x683c\x7d0d
    var text = str(clipboard())
    // \x51fa\x529b\x5148\x306e\x30bf\x30d6\x540d\x3092\x6307\x5b9a
    var tabNames = tab();
    var selected_index = dialog('.title', '\x3069\x306e\x30bf\x30d6\x306b\x51fa\x529b\x3057\x307e\x3059\x304b\xff1f', '.list:Select', tabNames)
    if (selected_index)
      tab(tabNames[selected_index])
    // \x6587\x5b57\x5217\x3092\x31\x884c\x305a\x3064\x914d\x5217\x306b\x683c\x7d0d\x3059\x308b
    var textArray = text.split(/\\r\\n|\\r|\\n/)
    // \x9006\x9806\x306b\x30b9\x30bf\x30c3\x30af
    for (var i = textArray.length - 1; i >= 0 ; i--){
      if (!textArray[i].trim())
        continue
      insert(0 , textArray[i].trim())
    }"
Icon=\xf24d
InMenu=true
Name=1\x884c\x31\x30a2\x30a4\x30c6\x30e0\x3068\x3057\x3066\x30b3\x30d4\x30fc

使い方

あらかじめ以下の記事を参考に、スクリプトを登録しておきます。

CopyQ に共有スクリプトを追加する方法 - In my mind

あらかじめ CopyQ を起動しておきます。

あらかじめ出力先のタブを作っておきます。

複数行のテキストを選択して CopyQ のトレイアイコンを右クリック。

メニューから「1行1アイテムとしてコピー」を選択。

1行1アイテムとしてコピー

「どのタブに出力しますか?」というダイアログが出てきたら出力先のタブを選択。

出力先のタブを選択

出力先のタブを開くと1行1アイテムとしてコピーされてます。

1行1アイテムとしてコピーされてます

参考

Scripting API — CopyQ documentation

JavaScript - 配列を逆順にループする方法 | ITライフ


アイキャッチ画像提供:A MacBook with lines of code on its screen on a busy desk photo – Free Computer Image on Unsplash

記事を書くのが楽になる!選択範囲内の画像タグを遅延ロードにする CopyQ スクリプト

f:id:dot-harley:20200110233155j:plain

はてなブログの記事編集画面、サイドバーから写真を貼り付けすると問答無用で「はてな記法」が挿入されますよね。

このブログは遅延読み込みライブラリの Lozad.js を利用しています。

Lozad.js を利用するため、はてな記法から HTML の img タグに直して class属性と data-src 属性を付けて src 属性を空にする、という作業を毎回毎回手打ちでおこなっていましたが、非常に面倒です。

そこでこれらの作業を自動化する CopyQ スクリプトを作ってみました。

Lozad.js についてはこちらの記事をご覧ください。

はてなブログに Lozad.js を導入して画像を遅延読み込みする方法 - In my mind

特徴

  • 選択範囲内のすべての画像タグに data-src 属性とclass="lozad"が追加され、src 属性は削除されます。

  • マークダウン形式の画像タグは HTML 形式に変換されます。

  • はてな記法の画像タグは HTML 形式に変換されます。

  • ついでに schema.org の構造化データが付きます。

  • 正規表現による置換を繰り返すことで実現してます。「もっとスマートなやり方があるよ」という方はぜひコメント欄にお願いします。

免責事項

無保証です。このスクリプトを使用して何らかの損害が発生したとしても、作者は責任を負いません。

不具合の報告はコメント欄にお願いします。直せるかどうか分かりませんが。。。

コード

[Command]
Command="
    copyq:
    if (!copy())
      abort()
    
    // \x9078\x629e\x7bc4\x56f2\x306e\x30c6\x30ad\x30b9\x30c8\x3092\x5909\x6570\x306b\x683c\x7d0d
    var text = str(clipboard())
    
    // Markdown image tag -> HTML
    var newText = text.replace(/!\\[(.*?)\\]\\((.*?)\\s*\\\"(.*?)\\\"\\)/g , '<img src=\"$2\" alt=\"$1\" title=\"$3\">')
    newText = newText.replace(/!\\[(.*?)\\]\\((.*?)\\)/g , '<img src=\"$2\" alt=\"$1\">')
    
    // hatena image tag -> HTML
    newText = newText.replace(/\\[(\\w):id:(.*?):(\\d{8})(\\d{6})(\\w):plain(:alt=.*?)?\\]/g , '<img src=\"//cdn-ak.f.st-hatena.com/images/fotolife/d/$2/$3/$3$4.$5\" alt=\"$1:id:$2:$3$4$5:plain$6\" title=\"$1:id:$2:$3$4$5:plain$6\" class=\"hatena-fotolife\">')
    newText = newText.replace(/<img (.*?)src=\"(.*?)\\.j\"(.*?)>/g , '<img $1src=\"$2.jpg\"$3>')
    newText = newText.replace(/<img (.*?)src=\"(.*?)\\.p\"(.*?)>/g , '<img $1src=\"$2.png\"$3>')
    newText = newText.replace(/<img (.*?)src=\"(.*?)\\.g\"(.*?)>/g , '<img $1src=\"$2.gif\"$3>')
    
    // img src -> data-src + class=\"lozad\"
    newText = newText.replace(/<img (?:data-)?(.*?)src=\"(.*?)\"(.*?)>/g , '<img $1data-src=\"$2\"$3 class=\"lozad\">')
    newText = newText.replace(/<img (.*?)class=\"(.*?)\"(.*?)class=\"(.*?)\"(.*?)>/g , '<img $1class=\"$2 $4\"$3$5>')
    newText = newText.replace(/<img (.*?)class=\"(.*?)\"(.*?)class=\"(.*?)\"(.*?)>/g , '<img $1class=\"$2 $4\"$3$5>')
    newText = newText.replace(/class=\"(.*?)lozad(.*?)lozad(.*?)/g , 'class=\"$1lozad$2$3')
    
    // schema.org
    newText = newText.replace(/<.*?itemscope.*?>[\\s\\n]*(<img .*?>)[\\s\\n]*<\\/.*?>/g , '$1')
    newText = newText.replace(/<img (.*?)itemprop=\".*?\"(.*?)>/g , '<img $1$2>')
    newText = newText.replace(/<img (.*?)>/g , '<span itemscope itemtype=\"http://schema.org/Photograph\"><img $1 itemprop=\"image\"></span>')
    
    // Remove trailing space
     newText = newText.replace(/ {2,}/g , ' ')
    
    if (text == newText)
      abort();
    
    //\x51fa\x6765\x4e0a\x304c\x3063\x305f\x3082\x306e\x3092\x8cbc\x308a\x4ed8\x3051
    copy(newText)
    paste()"
Icon=\xf121
InMenu=true
Name=img \x30bf\x30b0 \x9045\x5ef6\x30ed\x30fc\x30c9\x5316 (lozad.js)

使い方

あらかじめ以下の記事を参考に、スクリプトを登録しておきます。

CopyQ に共有スクリプトを追加する方法 - In my mind

あらかじめ CopyQ を起動しておきます。

テキストエディタやブログの記事編集画面で遅延ロードにしたい画像タグを含むように選択範囲を作ります。

選択範囲をつくる

デスクトップパネルの CopyQ アイコン(ハサミの形したやつ)を右クリック。

出てきたメニューからスクリプトをクリックすると実行されます。

スクリプトを実行

クリップボード<EMPTY>だとスクリプト名がメニューに表示されません。

何かテキトーなテキストでもコピーしておいてください。

参考

Change Upper/Lower Case of Selected Text


アイキャッチ画像提供:A MacBook with lines of code on its screen on a busy desk photo – Free Computer Image on Unsplash