<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shashank</title>
    <description>The latest articles on DEV Community by Shashank (@bekaar_coder).</description>
    <link>https://dev.to/bekaar_coder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F77396%2F196bd5b5-e323-41c5-acde-b8ebe6184a7f.jpeg</url>
      <title>DEV Community: Shashank</title>
      <link>https://dev.to/bekaar_coder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bekaar_coder"/>
    <language>en</language>
    <item>
      <title>Setup Jellyfin On Raspberry Pi - Home Media Server</title>
      <dc:creator>Shashank</dc:creator>
      <pubDate>Sun, 20 Apr 2025 16:05:00 +0000</pubDate>
      <link>https://dev.to/bekaar_coder/setup-jellyfin-on-raspberry-pi-home-media-server-4008</link>
      <guid>https://dev.to/bekaar_coder/setup-jellyfin-on-raspberry-pi-home-media-server-4008</guid>
      <description>&lt;p&gt;In this guide, we’ll walk through setting up a personal media server using &lt;code&gt;Jellyfin&lt;/code&gt; on a Raspberry Pi 5 running Ubuntu 24.04. We'll configure a USB drive as the media storage, mount it properly, auto-mount it at boot, and set up Samba so you can transfer media from your PC or laptop over the network. Great for anyone wanting a self-hosted Netflix-like experience.&lt;/p&gt;



&lt;h2&gt;
  
  
  Setting Up Jellyfin Media Server
&lt;/h2&gt;
&lt;h3&gt;
  
  
  📦 Step 1: Install Jellyfin
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://repo.jellyfin.org/install-debuntu.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once installed, Jellyfin runs on port &lt;code&gt;8096&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;Open it in your browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;your-pi-ip&amp;gt;:8096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before setting up Jellyfin, lets go through other steps to add the media folders in Jellyfin server.&lt;/p&gt;



&lt;h3&gt;
  
  
  🔌 Step 2: Connect Your USB Drive
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Plug in your USB drive.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ubuntu may &lt;strong&gt;auto-mount it&lt;/strong&gt; under:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/media/&amp;lt;your-username&amp;gt;/&amp;lt;usb-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;👉 This location is fine temporarily, but it can &lt;strong&gt;change&lt;/strong&gt; or fail to mount after a reboot — which is &lt;strong&gt;not reliable&lt;/strong&gt; for a media server.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔍 Step 3: Identify the USB Drive and Its Filesystem
&lt;/h3&gt;

&lt;p&gt;To list connected drives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get UUID and filesystem type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for your USB device, something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/sda1: UUID="1234-ABCD" TYPE="exfat"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the &lt;strong&gt;UUID&lt;/strong&gt; and note the &lt;strong&gt;TYPE&lt;/strong&gt; (e.g., &lt;code&gt;exfat&lt;/code&gt;, &lt;code&gt;ntfs&lt;/code&gt;, &lt;code&gt;ext4&lt;/code&gt;, or &lt;code&gt;vfat&lt;/code&gt;).&lt;/p&gt;



&lt;h3&gt;
  
  
  📂 Step 4: Create a Permanent Mount Point
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /media/usbdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;usbdrive&lt;/code&gt; can be anything. You can use your actual usb drive name for this.&lt;/p&gt;



&lt;h3&gt;
  
  
  🛠 Step 5: Install Filesystem Support (if needed)
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;exFAT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;exfat-fuse exfatprogs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;NTFS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ntfs-3g
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  ⚙️ Step 6: Configure Auto-Mount with &lt;code&gt;/etc/fstab&lt;/code&gt; (Based on Filesystem Type)
&lt;/h3&gt;

&lt;p&gt;Edit your fstab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ✅ For &lt;code&gt;exfat&lt;/code&gt; drives:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=1234-ABCD /media/usbdrive exfat uid=jellyfin,gid=jellyfin,umask=0022,nofail,x-systemd.automount 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ✅ For &lt;code&gt;ext4&lt;/code&gt; drives:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=1234-ABCD /media/usbdrive ext4 defaults,nofail,x-systemd.automount 0 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ✅ For &lt;code&gt;ntfs&lt;/code&gt; drives:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=1234-ABCD /media/usbdrive ntfs-3g uid=jellyfin,gid=jellyfin,umask=0022,nofail,x-systemd.automount 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ✅ For &lt;code&gt;vfat&lt;/code&gt; (FAT32) drives:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUID=1234-ABCD /media/usbdrive vfat uid=jellyfin,gid=jellyfin,umask=0022,nofail,x-systemd.automount 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving, mount all drives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check if the drive mounted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /media/usbdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  🔑 Step 7: Fix Permissions (if needed)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; jellyfin:jellyfin /media/usbdrive
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; 755 /media/usbdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h3&gt;
  
  
  🎞 Step 8: Add USB Folder to Jellyfin
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;http://&amp;lt;your-pi-ip&amp;gt;:8096&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Dashboard → Libraries&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Create a new library (e.g. "Movies")&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the path to:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/media/usbdrive
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Save and scan the library&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;{:.blockquote}&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re more old-school, you can simply disconnect the USB drive from your Raspberry Pi and connect it to another laptop to transfer files. But if you’d prefer to avoid that hassle and transfer files over the network, stick around — we’ll set up a Samba server to make file sharing seamless and easy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Transfering Files
&lt;/h2&gt;

&lt;p&gt;We can install Samba on Ubuntu to access the USB drive over the network from other PCs or laptops. However, currently only the &lt;code&gt;jellyfin&lt;/code&gt; user has full permission to modify or delete files on the USB drive.&lt;/p&gt;

&lt;p&gt;To fix this, we’ll create a shared group, add both the &lt;code&gt;jellyfin&lt;/code&gt; and &lt;code&gt;pi&lt;/code&gt; users to it, and then mount the USB drive using this shared group to ensure proper access for both users.&lt;/p&gt;

&lt;h3&gt;
  
  
  📁 Step 9. Mount USB Drive as a Shared Group
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create and assign group:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;groupadd media
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; media pi
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; media jellyfin
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;FSTAB entry:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;XXXX-XXXX  /media/usbdrive  exfat  defaults,uid&lt;span class="o"&gt;=&lt;/span&gt;pi,gid&lt;span class="o"&gt;=&lt;/span&gt;media,umask&lt;span class="o"&gt;=&lt;/span&gt;0002,nofail,x-systemd.automount  0  0
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reload:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /media/usbdrive
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To check ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /media/usbdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  🌐 Step 10. Set up Samba for File Transfers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Samba:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;samba samba-common-bin
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Create a new config block at the end of &lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/samba/smb.conf
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Add the below config&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[usbonpi]&lt;/span&gt;
  &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/media/usbdrive&lt;/span&gt;
  &lt;span class="py"&gt;writeable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="py"&gt;browseable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="py"&gt;public&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;no&lt;/span&gt;
  &lt;span class="err"&gt;read&lt;/span&gt; &lt;span class="py"&gt;only&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;no&lt;/span&gt;
  &lt;span class="err"&gt;guest&lt;/span&gt; &lt;span class="py"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;no&lt;/span&gt;
  &lt;span class="err"&gt;force&lt;/span&gt; &lt;span class="py"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pi&lt;/span&gt;
  &lt;span class="err"&gt;force&lt;/span&gt; &lt;span class="py"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;pi&lt;/span&gt;
  &lt;span class="err"&gt;create&lt;/span&gt; &lt;span class="py"&gt;mask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0777&lt;/span&gt;
  &lt;span class="err"&gt;directory&lt;/span&gt; &lt;span class="py"&gt;mask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0777&lt;/span&gt;
  &lt;span class="err"&gt;delete&lt;/span&gt; &lt;span class="py"&gt;readonly&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Reload Samba Service:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart smbd
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Add a Samba user:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;smbpasswd &lt;span class="nt"&gt;-a&lt;/span&gt; pi
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Access the share via another PC:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you are on Mac:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Finder&lt;/li&gt;
&lt;li&gt;Go to &lt;code&gt;Go&lt;/code&gt; &amp;gt; &lt;code&gt;Connect to Server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Type the following:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smb://&amp;lt;raspberrypi_ip&amp;gt;\usbonpi
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are on Windows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open File Explorer&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the address bar, type the following:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\\&amp;lt;raspberrypi_ip&amp;gt;\usbonpi
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;



&lt;h2&gt;
  
  
  📋 Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  We installed Jellyfin and accessed it via a browser.&lt;/li&gt;
&lt;li&gt;  A USB drive was configured to auto-mount using &lt;code&gt;fstab&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Permissions were assigned to allow both Jellyfin and the Samba-accessing user.&lt;/li&gt;
&lt;li&gt;  A Samba server was set up for easy file transfer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ You're All Set! 🎉&lt;/p&gt;

&lt;p&gt;Your Raspberry Pi is now a full-fledged Jellyfin server with a persistent, auto-mounted USB drive as the media library.&lt;/p&gt;

</description>
      <category>ubuntu</category>
      <category>raspberrypi</category>
      <category>jellyfin</category>
    </item>
    <item>
      <title>Deploy a Node.js Application Using MySQL and Prisma on a Raspberry Pi</title>
      <dc:creator>Shashank</dc:creator>
      <pubDate>Sun, 05 Jan 2025 06:22:00 +0000</pubDate>
      <link>https://dev.to/bekaar_coder/deploy-a-nodejs-application-using-mysql-and-prisma-on-a-raspberry-pi-4g4a</link>
      <guid>https://dev.to/bekaar_coder/deploy-a-nodejs-application-using-mysql-and-prisma-on-a-raspberry-pi-4g4a</guid>
      <description>&lt;p&gt;Deploying applications has become increasingly accessible, with a range of free and paid hosting options like Render, AWS, and DigitalOcean. However, for developers who want to learn, experiment, and deploy applications without recurring hosting fees, a &lt;strong&gt;Raspberry Pi&lt;/strong&gt; offers an excellent alternative. This compact yet powerful device allows you to create your own Linux-based server for hosting web applications.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll explore how to deploy a &lt;strong&gt;TypeScript Node.js&lt;/strong&gt; application using &lt;strong&gt;MySQL&lt;/strong&gt; (MariaDB on Raspberry Pi) and &lt;strong&gt;Prisma ORM&lt;/strong&gt; on a Raspberry Pi. Additionally, we’ll configure &lt;strong&gt;NGINX&lt;/strong&gt; for reverse proxying and use &lt;strong&gt;Ngrok&lt;/strong&gt; to expose the application to the internet. Let’s dive in!&lt;/p&gt;



&lt;h2&gt;
  
  
  Tools Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Raspberry Pi&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A low-cost, single-board computer that runs a Linux-based operating system. It’s ideal for creating your own server for IoT or web applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js &amp;amp; TypeScript&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Node.js is a runtime environment for executing JavaScript on the server, and TypeScript adds static typing to JavaScript, making the codebase more maintainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MySQL (MariaDB on Raspberry Pi)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A popular relational database system, MariaDB is a compatible replacement for MySQL and is lightweight enough for a Raspberry Pi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma ORM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An Object-Relational Mapping (ORM) tool that simplifies database interactions with a type-safe query language and schema migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NGINX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A high-performance HTTP server and reverse proxy server. It helps route traffic to your Node.js application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ngrok&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A tunneling tool that exposes your locally hosted applications to the internet securely without complex network configurations.&lt;/p&gt;



&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A working Raspberry Pi&lt;/strong&gt; - Ensure that SSH is enabled on the Raspberry Pi and you can access it remotely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Github Repository&lt;/strong&gt; - Your Node.js TypeScript application should be hosted in a GitHub repository for easy deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ngrok Account&lt;/strong&gt; - Create a free account on Ngrok to obtain an auth token for exposing your Raspberry Pi app to the internet.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Setting Up Raspberry Pi
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install the OS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Setup your Raspberry Pi with an OS like Raspberry Pi OS. Use the &lt;code&gt;Raspberry Pi Imager&lt;/code&gt; to find other OS compatible to your Raspberry Pi.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Find the IP Address&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use a tool like &lt;code&gt;Angry IP Scanner&lt;/code&gt; to discover your Raspberry Pi’s IP address. Ensure the Raspberry Pi is connected to the same network as your local machine.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Raspberry Pi Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;ping &amp;lt;IP_ADDRESS_OF_RPI&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;SSH Into the Raspberry Pi&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;IP_ADDRESS_OF_RPI&amp;gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; with your Raspberry Pi’s username and &lt;code&gt;&amp;lt;IP_ADDRESS_OF_RPI&amp;gt;&lt;/code&gt; with the IP address and then enter the password.&lt;/p&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Update the System&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Install Git&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check if git is installed. If not, run the below command to install git&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;git
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;



&lt;h3&gt;
  
  
  Installing Node.js
&lt;/h3&gt;

&lt;p&gt;To install node js, we will be using &lt;code&gt;nvm&lt;/code&gt; (Node Version Manager). It allows you to quickly install and use different version of node via command line.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install NVM&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Install the Latest LTS Version of Node.js&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--lts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Verify Node.js and npm Installation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# v22.12.0&lt;/span&gt;

npm &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# 10.9.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;



&lt;h2&gt;
  
  
  Setting Up MySQL (MariaDB)
&lt;/h2&gt;

&lt;p&gt;For Raspberry Pi OS, we will be installing MariaDB.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install the MariaDB SQL Server&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;mariadb-server
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Secure MariaDB Installation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Follow the prompts to secure your database.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enter the current root password:&lt;/strong&gt; - Press Enter when asked to enter the current password for the root user (since it hasn’t been set yet).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the root password:&lt;/strong&gt; - Type n when prompted to set the root password (we’ll set it later).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove anonymous users:&lt;/strong&gt; - Type Y to remove anonymous users and improve security. (For testing purposes, you can type n to keep anonymous users.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disallow root login remotely:&lt;/strong&gt; - Type n to allow root login remotely (optional but less secure).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the test database:&lt;/strong&gt; - Type y to remove the test database and access to it. (Type n if you want to keep it.)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Login to MariaDB Client&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Setup a Root password for MariaDB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, we need to tell the database server to reload the grant tables.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;MariaDB &lt;span class="o"&gt;[(&lt;/span&gt;none&lt;span class="o"&gt;)]&amp;gt;&lt;/span&gt; FLUSH PRIVILEGES&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Change the root password with below query.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;MariaDB &lt;span class="o"&gt;[(&lt;/span&gt;none&lt;span class="o"&gt;)]&amp;gt;&lt;/span&gt; ALTER USER &lt;span class="s1"&gt;'root'&lt;/span&gt;@&lt;span class="s1"&gt;'localhost'&lt;/span&gt; IDENTIFIED BY &lt;span class="s1"&gt;'&amp;lt;new_password&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;new_password&amp;gt;&lt;/code&gt; with your own password.&lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;exit&lt;/code&gt; command to exit from MariaDB CLI.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;MariaDB &lt;span class="o"&gt;[(&lt;/span&gt;none&lt;span class="o"&gt;)]&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Bye
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Login to MariaDB Client With Root User&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; root &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Enter the password for the root user.&lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setting up Database and User
&lt;/h3&gt;

&lt;p&gt;Let's create a new database and a user. We will be granting all privileges to the new user for the new database we have created.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a Database&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; CREATE DATABASE &amp;lt;database_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create a New User With Password&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; CREATE USER '&amp;lt;new_username&amp;gt;'@'localhost' IDENTIFIED BY '&amp;lt;new_password&amp;gt;';
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Grant Privilege To New User Created&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; GRANT ALL PRIVILEGES ON &amp;lt;database_name&amp;gt;.* TO '&amp;lt;new_username&amp;gt;'@'localhost';
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Flush The Privileges Table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; FLUSH PRIVILEGES;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exit from mysql client using &lt;code&gt;exit&lt;/code&gt; command.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Login With New User&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; &amp;lt;new_username&amp;gt; &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Enter the password you used while creating the user.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verify User Can List The Database&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; SHOW DATABASES;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it! We will use this database and user in our application.&lt;/p&gt;



&lt;h2&gt;
  
  
  Setup your Node.js Application
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clone Your Github Repository&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;your_github_repo_url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Navigate To Your Project Repository&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;your_project_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Install Project Dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Compile TypeScript Code&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure you have configured the &lt;code&gt;outDir&lt;/code&gt; property in your &lt;code&gt;tsconfig.json&lt;/code&gt; file. This specifies the directory where the compiled JavaScript code will be generated. By default, it’s commonly set to &lt;code&gt;dist&lt;/code&gt;, but you can customize it based on your project structure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Setting up environment variables (Optional)
&lt;/h3&gt;

&lt;p&gt;If your project uses environment variables, you need to set them on your Raspberry Pi. You can create a &lt;code&gt;.env&lt;/code&gt; file in the root directory of your project to store all the environment variables.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create &lt;code&gt;.env&lt;/code&gt; File&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update &lt;code&gt;.env&lt;/code&gt; File&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enter your Environment Variables&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT=5000
DATABASE_URL="mysql://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@localhost:3306/&amp;lt;database_name&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Replace the &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;password&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;database_name&amp;gt;&lt;/code&gt; with the one you created in the previous steps.&lt;/p&gt;

&lt;p&gt;Save the file by pressing &lt;code&gt;Ctrl+O&lt;/code&gt;, then press &lt;code&gt;Enter&lt;/code&gt;, and exit the editor using &lt;code&gt;Ctrl+X&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Migrate Prisma Schema
&lt;/h3&gt;

&lt;p&gt;If you are using Prisma, all the schema files will be located inside the prisma/schema directory. We will now deploy these schemas to the database.&lt;/p&gt;

&lt;p&gt;Run the below command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prisma migrate deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will use the &lt;code&gt;DATABASE_URL&lt;/code&gt; provided in the &lt;code&gt;.env&lt;/code&gt; file to deploy the schemas to the database. You can verify the deployment by logging into the MySQL client and using the command &lt;code&gt;SHOW TABLES;&lt;/code&gt; to list all the tables.&lt;/p&gt;



&lt;h2&gt;
  
  
  Setting Up PM2
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PM2&lt;/code&gt; is a production process manager for Node.js applications which helps in managing and keeping the application online. Install PM2 to manage your Node.js application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;pm2 &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  Configuring NGINX
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install NGINX&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Create a Site Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/&amp;lt;your_project_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Add the Below Code&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;
    server_name &amp;lt;your_raspberrypi_IP&amp;gt;;

    location / {
        proxy_pass http://localhost:YOUR_NODE_JS_PORT;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Here’s a breakdown of each part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;listen 80;&lt;/code&gt;&lt;/strong&gt; This directive tells NGINX to listen on port 80, which is the default port for HTTP traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server_name &amp;lt;your_raspberrypi_IP&amp;gt;;&lt;/code&gt;&lt;/strong&gt; This specifies the domain name or IP address of your Raspberry Pi. Replace  with the actual IP address of your Raspberry Pi. NGINX will respond to requests sent to this address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;location / { ... }&lt;/code&gt;&lt;/strong&gt; This block defines how NGINX should handle requests to the root URL (/). Essentially, this tells NGINX that whenever a request is made to the root, it should be forwarded to the backend (your Node.js application) running on the specified port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_pass http://localhost:YOUR_NODE_JS_PORT;&lt;/code&gt;&lt;/strong&gt; This is the key line that forwards incoming requests to your Node.js application. Replace YOUR_NODE_JS_PORT with the actual port where your Node.js app is running (for example, 5000). The requests will be sent to the Node.js application running on the same machine (localhost).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_http_version 1.1;&lt;/code&gt;&lt;/strong&gt; This sets the HTTP version to 1.1 for the proxy connection, which ensures better handling of certain features like WebSockets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_set_header Upgrade $http_upgrade;&lt;/code&gt;&lt;/strong&gt; This header allows WebSocket connections to be upgraded, which is important for real-time applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_set_header Connection 'upgrade';&lt;/code&gt;&lt;/strong&gt; This header is used alongside the Upgrade header to manage WebSocket connections, ensuring that the connection is properly upgraded from HTTP to WebSocket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_set_header Host $host;&lt;/code&gt;&lt;/strong&gt; This passes the original Host header from the client request to the backend server. This is useful for applications that rely on the original Host header (e.g., for routing or virtual hosting).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxy_cache_bypass $http_upgrade;&lt;/code&gt;&lt;/strong&gt; This ensures that WebSocket connections bypass any caching mechanisms, allowing real-time communication to work without interference from caching.&lt;/p&gt;

&lt;p&gt;Save the file by pressing &lt;code&gt;Ctrl+O&lt;/code&gt;, then press &lt;code&gt;Enter&lt;/code&gt;, and exit the editor using &lt;code&gt;Ctrl+X&lt;/code&gt;.&lt;/p&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Enable the Site Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/&amp;lt;your_project_name&amp;gt; /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Test NGINX Configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If the test is successfull, you will see something like below:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Restart NGINX Server To Apply the Changes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Check NGINX Server Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;



&lt;h2&gt;
  
  
  Running the Application
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Navigate to your project&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Your Application Using PM2&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have setup a script in &lt;code&gt;package.json&lt;/code&gt;, use the below command:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start &lt;span class="s2"&gt;"npm run start"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Or, you can directly run you application using &lt;code&gt;index.js&lt;/code&gt; file in your &lt;code&gt;dist&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start dist/index.js
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;You can also check the logs using below command:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 logs
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;Now, check your app by entering the IP address of your Raspberry Pi in the browser on your local machine. It should work. Make sure both your local machine and Raspberry Pi are connected to the same network; otherwise, it will not work.&lt;/p&gt;



&lt;h2&gt;
  
  
  Exposing Your App To The World Using Ngrok
&lt;/h2&gt;

&lt;p&gt;Now that you have deployed your app to the Raspberry Pi, you can only access the app from the same network in which the Raspberry Pi is running. To expose it to the internet, we need to use port forwarding.&lt;/p&gt;

&lt;p&gt;You can set up port forwarding using your router settings, but in this case, I will be using ngrok. &lt;code&gt;Ngrok&lt;/code&gt; is useful for development, allowing us to run our apps for testing purposes for free.&lt;/p&gt;

&lt;p&gt;Make sure to create an account by visiting &lt;a href="https://dashboard.ngrok.com/login" rel="noopener noreferrer"&gt;https://dashboard.ngrok.com/login&lt;/a&gt;. You will need the auth token to configure ngrok on the Raspberry Pi.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Ngrok&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://ngrok-agent.s3.amazonaws.com/ngrok.asc &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/trusted.gpg.d/ngrok.asc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://ngrok-agent.s3.amazonaws.com buster main"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/ngrok.list &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ngrok
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Add your auth token to ngrok configuration file&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok config add-authtoken &amp;lt;your_auth_token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Disable default nginx config file&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Test NGINX configuration&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Restart NGINX server to apply the changes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Deploy your app online&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 80
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;This should provide a URL like &lt;code&gt;https://xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx.ngrok-free.app/&lt;/code&gt; that forwards traffic to your Node.js app. You can navigate to this URL from any other network and access your application.&lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;



&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;In this guide, we successfully deployed a TypeScript Node.js application with MySQL and Prisma on a Raspberry Pi. We configured NGINX as a reverse proxy and used Ngrok to make the application accessible over the internet. With this setup, you have your own cost-effective, self-hosted development server.&lt;/p&gt;

&lt;p&gt;This approach is perfect for learning and experimenting with full-stack application deployment, all while gaining valuable experience in server management.&lt;/p&gt;

&lt;p&gt;Let me know if you deploy your application using this guide—I’d love to hear about your experience! 🚀&lt;/p&gt;

</description>
      <category>node</category>
      <category>mysql</category>
      <category>raspberrypi</category>
      <category>ngrok</category>
    </item>
    <item>
      <title>How to Scrape Data From Goodreads Using Python and BeautifulSoup</title>
      <dc:creator>Shashank</dc:creator>
      <pubDate>Fri, 06 Dec 2024 17:58:00 +0000</pubDate>
      <link>https://dev.to/bekaar_coder/how-to-scrape-data-from-goodreads-using-python-and-beautifulsoup-4mf9</link>
      <guid>https://dev.to/bekaar_coder/how-to-scrape-data-from-goodreads-using-python-and-beautifulsoup-4mf9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Web scraping&lt;/strong&gt; is a powerful tool for gathering data from websites. Whether you’re collecting product reviews, tracking prices, or, in our case, scraping Goodreads books, web scraping provides endless opportunities for data-driven applications.&lt;/p&gt;

&lt;p&gt;In this blog post, we’ll explore the fundamentals of web scraping, the power of the Python &lt;code&gt;BeautifulSoup&lt;/code&gt; library, and break down a Python script designed to scrape Goodreads Choice Awards data. Finally, we’ll discuss how to store this data in a CSV file for further analysis or applications.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is Goodreads?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Goodreads is the world’s largest platform for readers and book recommendations. It provides users with access to book reviews, author details, and popular rankings. Every year, Goodreads hosts the Goodreads Choice Awards, where readers vote for their favorite books across various genres like fiction, fantasy, romance, and more. This makes Goodreads an ideal target for web scraping to gather insights about trending books and authors.&lt;/p&gt;



&lt;h3&gt;
  
  
  What is Web Scraping?
&lt;/h3&gt;

&lt;p&gt;Web scraping involves extracting data from websites in an automated manner. It allows you to collect and structure information for tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Analyzing trends and patterns.&lt;/li&gt;
&lt;li&gt;  Aggregating content like reviews or articles.&lt;/li&gt;
&lt;li&gt;  Feeding machine learning models or databases.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Setting Up Your Environment
&lt;/h2&gt;

&lt;p&gt;Before diving into the script, you need to install the necessary libraries.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure you have Python installed on your system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Required Libraries&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install the required libraries using &lt;code&gt;pip&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4
pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;code&gt;request&lt;/code&gt;: Allows us to send HTTP requests to a URL and retrieve the web page’s content.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BeautifulSoup&lt;/code&gt;: Simplifies HTML parsing and data extraction.&lt;/p&gt;


&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;Once these installations are complete, you're ready to scraping!&lt;/p&gt;



&lt;h3&gt;
  
  
  Introduction to BeautifulSoup
&lt;/h3&gt;

&lt;p&gt;BeautifulSoup is a Python library for parsing HTML and XML documents. It enables developers to navigate page structures, extract content, and transform raw HTML into a structured format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Methods in BeautifulSoup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here are a few essential methods that we will be using in our script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;BeautifulSoup(html, 'html.parser')&lt;/code&gt;: Initializes the parser and allows you to work with the HTML content.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;soup.select(selector)&lt;/code&gt;: Finds elements using CSS selectors, such as classes or tags.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;soup.find(class_='class_name')&lt;/code&gt;: Locates the first occurrence of an element with a specified class.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;soup.find_parent(class_='class_name')&lt;/code&gt;: Finds the parent tag of the current element.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;soup.get('attribute')&lt;/code&gt;: Retrieves the value of an attribute from an element, like href or src.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a complete list of methods, check out the &lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;BeautifulSoup documentation&lt;/a&gt;.&lt;/p&gt;



&lt;h3&gt;
  
  
  Setting Up the Script
&lt;/h3&gt;

&lt;p&gt;Let’s begin by importing the necessary libraries and defining custom headers to mimic a browser. This helps avoid getting blocked by the website.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;

&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (X11; Linux x86_64)...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US, en;q=0.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scraping Categories and Books
&lt;/h3&gt;

&lt;p&gt;We start by defining the URLs for Goodreads’ Choice Awards page and the main application. We will send a request to &lt;code&gt;start_url&lt;/code&gt; and get the web page's content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;app_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.goodreads.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;start_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.goodreads.com/choiceawards/best-books-2024&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each category contains a genre and a link to its respective page. Using &lt;code&gt;soup.select&lt;/code&gt;, we extract all categories listed under the &lt;code&gt;.category&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1likz4f3dw3rcxjgcux.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1likz4f3dw3rcxjgcux.png" alt="Finding the parent category element" width="778" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, iterate through each category to get the genre name and its page URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;genre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h4.category__copy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;category_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;app_url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we extract the category name (genre) and the category page URL for further processing.&lt;/p&gt;

&lt;p&gt;We will send another request to each &lt;code&gt;category_url&lt;/code&gt; and locate all the books under that category.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;category_books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.resultShown a.pollAnswer__bookLink&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;category_books&lt;/code&gt; will contain the list of all the books under the respective category.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting Book Data
&lt;/h3&gt;

&lt;p&gt;Once we have the list of books, we will be iterating over each books and extract the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract Votes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;book_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category_books&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parent_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resultShown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;votes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parent_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;book_votes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;votes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we see in the DOM, voting count is present in the parent element of the category element. So we need to use &lt;code&gt;find_parent&lt;/code&gt; method to locate the element and extract the voting count.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4f7hbdhnjhdwxjjc5df5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4f7hbdhnjhdwxjjc5df5.png" alt="Finding the parent vote element" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extract Book Title, Author and Image URL&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;book_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;book_url_formatted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;app_url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;book_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;book_img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;img&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;book_img_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book_img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;book_img_alt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book_img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;book_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clean_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_img_alt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;book_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book_title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;book_author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book_title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each book's URL, cover image URL, title and author are extracted.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;clean_string&lt;/code&gt; function ensures the title is neatly formatted. You can define it at the top of the script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Extract More Book Details&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To get more details about the book like rating, reviews, etc., we will be sending another request to &lt;code&gt;book_url_formatted&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_url_formatted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;book_rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RatingStatistics__rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_rating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;book_ratings_reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RatingStatistics__meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aria-label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;book_ratings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;book_reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_ratings_reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_ratings_reviews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ratings: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;book_ratings&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, Reviews: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;book_reviews&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;book_description_elements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.BookPageMetadataSection__description .Formatted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;book_description_elements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;book_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;book_description_elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;book_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;No description found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;get_ratings_reviews&lt;/code&gt; returns the ratings and reviews text well formatted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3w1ms17l50scnc252xe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3w1ms17l50scnc252xe.png" alt="Rating and Reviews element" width="800" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can define this function at the top of the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_ratings_reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Find the substring for ratings
&lt;/span&gt;    &lt;span class="n"&gt;ratings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; ratings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Find the substring for reviews
&lt;/span&gt;    &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ratings&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By navigating to each book’s details page, additional information like ratings, reviews, and detailed descriptions is extracted. Here, we are also checking if book description element exists otherwise putting a default description so that the script does not fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;author_avatar_url_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.PageSection .AuthorPreview a.Avatar img.Avatar__image&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;author_avatar_url_element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;author_avatar_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author_avatar_url_element&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;author_avatar_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;No Avatar found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;author_description_element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.PageSection &amp;gt; .TruncatedContent .Formatted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;author_description_element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;author_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;author_description_element&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;author_description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;No description found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bookPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.FeaturedDetails p[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pagesFormat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bookPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pages_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookPages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;bookPages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pages_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No pages found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages_format&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;publication_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.FeaturedDetails p[data-testid=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publicationInfo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;publication_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;publication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publication_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;publication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No publication found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publication&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we have also gathered author details, publication information and other metadata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a Book Dictionary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let's store all the data we have extracted for a book in a dictionary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;book_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;genre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;votes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_votes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author_about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;author_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avatar_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;author_avatar_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pages_format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ratings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_ratings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_reviews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publication_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;publication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;img_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;book_img_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;book_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;app_url&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;book_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use this dictionary to add the data in a csv file.&lt;/p&gt;



&lt;h3&gt;
  
  
  Storing Data in a CSV File
&lt;/h3&gt;

&lt;p&gt;We will use the &lt;code&gt;csv&lt;/code&gt; module which is a part of Python's standard library. So you don't need to install it separately.&lt;/p&gt;

&lt;p&gt;First we need to check if this is the first entry. This check is required to add the header in the csv file in the first row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;csv_filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;books.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;book_index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;book_dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are using &lt;code&gt;mode="w"&lt;/code&gt; which will create a new csv file with the header entry.&lt;/p&gt;

&lt;p&gt;Now for all subsequent entries, we will append the data to the CSV file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;book_dict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mode="a"&lt;/code&gt; will append the data to CSV file.&lt;/p&gt;

&lt;p&gt;Now, sit back, relax, and enjoy a cup of coffee ☕️ while the script runs.&lt;/p&gt;

&lt;p&gt;Once it’s done, the final data will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff28kmqhdwad91ilvgw21.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff28kmqhdwad91ilvgw21.png" alt="Final CSV File Data" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the complete source code in this &lt;a href="https://github.com/bekaarcoder/goodreads-scraper" rel="noopener noreferrer"&gt;github repository&lt;/a&gt;.&lt;/p&gt;



&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;We have learned how to scrape Goodreads data using Python and BeautifulSoup. Starting from basic setup to storing data in a CSV file, we explored every aspect of the scraping process. The scraped data can be used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Data visualization (e.g., most popular genres or authors).&lt;/li&gt;
&lt;li&gt;  Machine learning models to predict book popularity.&lt;/li&gt;
&lt;li&gt;  Building personal book recommendation systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Web scraping opens up possibilities for creative data analysis and applications. With libraries like BeautifulSoup, even complex scraping tasks become manageable. Just remember to follow ethical practices and respect the website’s terms of service while scraping!&lt;/p&gt;

</description>
      <category>python</category>
      <category>beautifulsoup</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How to Run Playwright Tests in Jenkins Pipeline Using Docker</title>
      <dc:creator>Shashank</dc:creator>
      <pubDate>Sun, 01 Dec 2024 14:09:00 +0000</pubDate>
      <link>https://dev.to/bekaar_coder/running-playwright-tests-in-jenkins-pipeline-using-docker-3p96</link>
      <guid>https://dev.to/bekaar_coder/running-playwright-tests-in-jenkins-pipeline-using-docker-3p96</guid>
      <description>&lt;p&gt;Automation testing has become an integral part of modern software development, and tools like Playwright have made it seamless to perform end-to-end testing. However, executing these tests in a continuous integration pipeline can be challenging without proper setup. In this guide, we will go through the steps to run Playwright tests in a Jenkins pipeline using Docker, ensuring an efficient and scalable testing process. By the end of this post, you’ll have a clear understanding of setting up Jenkins with Docker, connecting agent nodes, and creating pipelines to build and run tests.&lt;/p&gt;



&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Docker Desktop&lt;/code&gt; installed on your local machine.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Docker Hub&lt;/code&gt; account created for managing Docker images.&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Jenkins Controller/Agent Architecture
&lt;/h2&gt;

&lt;p&gt;Before starting, let us look at the architecture. Jenkins &lt;code&gt;Controller/Agent Architecture&lt;/code&gt; allows distributed builds, where tasks are delegated to agents for execution. This architecture improves scalability and resource utilization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfo00ti1h5tobch8d8z8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfo00ti1h5tobch8d8z8.png" alt="Jenkins Architecture" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Controller Node&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Controller is the central component of the Jenkins architecture.&lt;/li&gt;
&lt;li&gt;  It handles:

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Job scheduling:&lt;/strong&gt; Assigns tasks to agent nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User interface:&lt;/strong&gt; Provides a web-based UI (port 8080) for users to interact with Jenkins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job configuration:&lt;/strong&gt; Allows users to define build pipelines and workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result aggregation:&lt;/strong&gt; Collects and displays build/test results.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;  It is not recommended to execute builds directly in the controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Agent Node&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The agents are distributed machines responsible for executing Jenkins jobs.&lt;/li&gt;
&lt;li&gt;  These nodes are connected to the master via the Jenkins Remoting protocol over port 50000.&lt;/li&gt;
&lt;li&gt;  They can execute builds or tests depending on the tasks assigned by the master.&lt;/li&gt;
&lt;li&gt;  Agents must have the necessary tools installed based on the jobs they run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Communication Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User interact with the Controller.&lt;/li&gt;
&lt;li&gt;Controller delegates the tasks to Agents.&lt;/li&gt;
&lt;li&gt;Agents execute the tasks.&lt;/li&gt;
&lt;li&gt;Results are sent back to the Controller&lt;/li&gt;
&lt;/ol&gt;



&lt;h2&gt;
  
  
  Setting Up Jenkins Using Docker
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Docker Compose Setup for Jenkins
&lt;/h3&gt;

&lt;p&gt;To set up Jenkins using Docker, create a docker-compose.yml file. Organize your directory as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jenkins-docker/
├── docker-compose.yml
└── volumes/
    ├── master/
    └── node/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the docker-compose.yml file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jenkins/jenkins:lts-jdk17&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8080:8080&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;50000:50000&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./volumes/master:/var/jenkins_home&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;JAVA_OPTS="-Dhudson.model.DirectoryBrowserSupport.CSP="&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;jenkins&lt;/code&gt;: Define service which will be created by Docker Compose.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;jenkins/jenkins:lts-jdk17&lt;/code&gt;: Docker image to create the container&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;user&lt;/code&gt;: We have specified user as &lt;code&gt;root&lt;/code&gt; to run the container with root privileges. This is necessary for certain administrative tasks (like installing plugins, setting permissions, or managing files)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;ports&lt;/code&gt; will map the ports on the host to the container. &lt;code&gt;8080:8080&lt;/code&gt; maps the Jenkins web UI. &lt;code&gt;50000:50000&lt;/code&gt; maps the port used for Jenkins agent communication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;volumes&lt;/code&gt;: Mounts a host directory (./volumes/master) to the container's Jenkins home directory (/var/jenkins_home). This will persist Jenkins data even if the container is deleted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;JAVA_OPTS&lt;/code&gt;: Configure JVM options for Jenkins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;-Dhudson.model.DirectoryBrowserSupport.CSP=&lt;/code&gt;: Disables Jenkins Content Security Policy. Although not recommended in production, but useful for embedding resources like images or styles in Jenkins view.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Running Docker Compose
&lt;/h3&gt;

&lt;p&gt;To start Jenkins, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once Jenkins service has started, you can acces Jenkins at &lt;code&gt;http://localhost:8080&lt;/code&gt; on your host machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Jenkins Web Interface
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt; in your browser.&lt;/li&gt;
&lt;li&gt;Use the administrator password from the terminal (output during docker-compose up).&lt;/li&gt;
&lt;li&gt;Install suggested plugins.&lt;/li&gt;
&lt;li&gt;Create the first admin user and complete the setup wizard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy5nx3yy0m11ndesjnbk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy5nx3yy0m11ndesjnbk.png" alt="Jenkins Password" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once complete, the Jenkins files will be available in the &lt;code&gt;volumes/master&lt;/code&gt; directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting An Agent Node
&lt;/h3&gt;

&lt;p&gt;Follow these steps to connect an agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the Jenkins homepage, click on &lt;code&gt;Set up an agent&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;NODE1&lt;/code&gt; as the node name, select &lt;code&gt;Permanent Agent&lt;/code&gt;, and configure the Remote root directory with the path to the node directory on your host.&lt;/li&gt;
&lt;li&gt;Leave other fields to default and click on Save.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now you will see a new node &lt;code&gt;NODE1&lt;/code&gt; is created which will be displayed with the Built-in Node.&lt;/p&gt;

&lt;p&gt;Click on the &lt;code&gt;NODE1&lt;/code&gt; and you will see the instructions to run the agent.&lt;/p&gt;

&lt;p&gt;Navigate to &lt;code&gt;node&lt;/code&gt; directory in your local machine from the terminal and run the commands provided by Jenkins to connect the node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; &lt;em&gt;Set the number of executors for the Built-in Node to 0 to ensure jobs only run on connected agents.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;code&gt;Built-in Node&lt;/code&gt; &amp;gt; &lt;code&gt;Configure&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Number of executers&lt;/code&gt; to 0 and click on Save.&lt;/li&gt;
&lt;/ol&gt;



&lt;h2&gt;
  
  
  Running Playwright Test With Jenkins
&lt;/h2&gt;

&lt;p&gt;You can use the &lt;a href="https://github.com/bekaarcoder/playwright-bdd-ts" rel="noopener noreferrer"&gt;Playwright Cucumber Framework&lt;/a&gt; or your own Playwright project. For this guide, we will use Docker to build the test environment and Jenkins to execute the tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This framework is built using Playwright and Cucumber. Instead of using the default Playwright command to execute tests, a custom &lt;code&gt;index.ts&lt;/code&gt; file has been created to run the tests using the Cucumber runner.&lt;/p&gt;

&lt;p&gt;To execute the tests, run the command &lt;code&gt;npm run cucumber &amp;lt;tag_name&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Predefined tags, such as &lt;code&gt;smoke&lt;/code&gt;, &lt;code&gt;regression&lt;/code&gt;, etc., are specified in the &lt;code&gt;index.ts&lt;/code&gt; file. Replace &lt;code&gt;&amp;lt;tag_name&amp;gt;&lt;/code&gt; with one of these predefined values to run the corresponding test suite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We need to install below plugins inside Jenkins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Docker&lt;/li&gt;
&lt;li&gt;  Docker Pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To install the plugins,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;code&gt;Manage Jenkins&lt;/code&gt; &amp;gt; &lt;code&gt;Plugins&lt;/code&gt; &amp;gt; &lt;code&gt;Available Plugins&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Search for the above plugins and install&lt;/li&gt;
&lt;li&gt;Once installed, restart jenkins&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To run jenkins again, run &lt;code&gt;docker-compose up&lt;/code&gt; again.&lt;/p&gt;
&lt;h3&gt;
  
  
  Creating a Dockerfile for Playwright
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/playwright:v1.48.2-noble&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["sh", "-c", "npm run cucumber ${TEST_TARGET}"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Base Image&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;FROM mcr.microsoft.com/playwright:v1.48.2-noble&lt;/code&gt; uses Playwright docker image to build our docker image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set Working Directory&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;WORKDIR /app&lt;/code&gt; sets the working directory inside the container to &lt;code&gt;/app&lt;/code&gt;. All subsequent commands and operations (e.g., COPY, RUN) will be executed relative to this directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy Files&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;COPY . /app&lt;/code&gt; Copies the contents of the current directory (on the host) into the container’s /app directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install Dependencies&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;RUN npm install&lt;/code&gt; Installs Node.js dependencies from the package.json file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entry Point&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;ENTRYPOINT&lt;/code&gt; Defines the command to execute when the container starts.&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting Up Docker Hub Credentials In Jenkins
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;code&gt;Manage Jenkins&lt;/code&gt; &amp;gt; &lt;code&gt;Credentials&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;(global)&lt;/code&gt; under Domains column.&lt;/li&gt;
&lt;li&gt;Select Kind as &lt;code&gt;Username with password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Enter username and password of Docker Hub.&lt;/li&gt;
&lt;li&gt;Enter ID (e.g., dockerhub-creds. This same id will be used to fetch username and password in the Jenkinsfile).&lt;/li&gt;
&lt;li&gt;Click Create.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Jenkins Pipeline for Building Docker Image
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;Jenkinsfile&lt;/code&gt; in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipeline {
    agent any

    stages {
        stage('Build Image') {
            steps {
                sh "docker build -t=&amp;lt;username&amp;gt;/playwright:latest ."
            }
        }

        stage('Push Image') {
            environment {
                DOCKER_HUB = credentials('dockerhub-creds')
            }
            steps {
                sh 'echo ${DOCKER_HUB_PSW} | docker login -u ${DOCKER_HUB_USR} --password-stdin'
                sh "docker push &amp;lt;username&amp;gt;/playwright:latest"
                sh "docker tag &amp;lt;username&amp;gt;/playwright:latest &amp;lt;username&amp;gt;/playwright:${env.BUILD_NUMBER}"
                sh "docker push &amp;lt;username&amp;gt;/playwright:${env.BUILD_NUMBER}"
            }
        }
    }

    post {
        always {
            sh "docker logout"
            sh "docker system prune -f"
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agent any&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Specifies that the pipeline can run on any available Jenkins agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pipeline has two primary stages:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stage('Build Image')&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Builds a docker image from the Dockerfile in the current directory (.).&lt;/li&gt;
&lt;li&gt;  Tags the image as &lt;code&gt;&amp;lt;username&amp;gt;/playwright:latest&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;*&lt;em&gt;Note: *&lt;/em&gt; &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; refers to your Docker Hub username. Replace &lt;code&gt;&amp;lt;username&amp;gt;&lt;/code&gt; with your actual Docker Hub username wherever it is mentioned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;stage('Push Image')&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Uses Jenkins credentials (dockerhub-creds) for authenticating with Docker Hub.

&lt;ul&gt;
&lt;li&gt;  ${DOCKER_HUB_USR}: The Docker Hub username.&lt;/li&gt;
&lt;li&gt;  ${DOCKER_HUB_PSW}: The Docker Hub password.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;code&gt;docker push &amp;lt;username&amp;gt;/playwright:latest&lt;/code&gt; pushes the latest version of the Docker image to Docker Hub&lt;/li&gt;

&lt;li&gt;  &lt;code&gt;docker tag &amp;lt;username&amp;gt;/playwright:latest &amp;lt;username&amp;gt;/playwright:${env.BUILD_NUMBER}&lt;/code&gt; tags the image with the jenkins build number (e.g., 1, 2, etc.)&lt;/li&gt;

&lt;li&gt;  &lt;code&gt;docker push &amp;lt;username&amp;gt;/playwright:${env.BUILD_NUMBER}&lt;/code&gt; pushed the versioned image to Docker Hub&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Post&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;post&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;docker logout&lt;/code&gt; logs out from Docker Hub&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;docker system prune -f&lt;/code&gt; cleans up unused docker data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building a Docker Image in Jenkins pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Create a New Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the Jenkins homepage and click on New Item.&lt;/li&gt;
&lt;li&gt;Enter an Item Name (e.g., Playwright-Docker-Build) and select &lt;code&gt;Pipeline&lt;/code&gt; and click OK.&lt;/li&gt;
&lt;li&gt;Under Pipeline Definition, select &lt;code&gt;Pipeline script from SCM&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select SCM to &lt;code&gt;git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Provide the Repository URL. (&lt;em&gt;For private repositories, set up credentials for GitHub in Jenkins and select the credentials here.&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Specify &lt;code&gt;Branch Specifier&lt;/code&gt; for your repository. (e.g., &lt;code&gt;*/main&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Under Additional Behaviours, choose &lt;code&gt;Clean before checkout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set Script Path as &lt;code&gt;Jenkinsfile&lt;/code&gt; (ensure the Jenkinsfile is located in the root directory of your repository).&lt;/li&gt;
&lt;li&gt;Click on Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Build the Image&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Click on &lt;code&gt;Build Now&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Once the build is successful, verify your Docker Hub repository. A Docker image with the name /playwright:latest should be pushed to your Docker Hub.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Setting Up Jenkins Pipeline for Running Tests
&lt;/h3&gt;

&lt;p&gt;For running our tests, we will be creating a separate jenkins pipeline. Create another directory for running tests (e.g., playwright-runner) and include Jenkinsfile and Docker Compose file.&lt;/p&gt;

&lt;p&gt;In the root directory, create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;playwright-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;username&amp;gt;/playwright&lt;/span&gt;
        &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;TEST_TARGET&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;BROWSER_CHOICE&lt;/span&gt;
        &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./reports:/app/reports&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Services&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;playwright-test&lt;/code&gt;: defines the service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Image&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;image&lt;/code&gt;: We will be using the prebuild image &lt;code&gt;&amp;lt;username&amp;gt;/playwright&lt;/code&gt; which contains the Playwright test environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environment Variables&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;environment&lt;/code&gt;: Passes enviroment variables to the container.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;TEST_TARGET&lt;/code&gt;: This will be used to pass the specific tags to run the test.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;BROWSER_CHOICE&lt;/code&gt;: This will be used to pass the specific browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Volumes&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;volumes&lt;/code&gt;: Mounts &lt;code&gt;./reports&lt;/code&gt; directory on host to &lt;code&gt;/app/reports&lt;/code&gt; directory on container.&lt;/p&gt;

&lt;p&gt;In the same directory, create a &lt;code&gt;Jenkinsfile&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipeline {
    agent any

    parameters {
        choice choices: ['login', 'smoke', 'regression', 'faker'], name: 'TEST_TARGET'
        choice choices: ['chromium', 'firefox'], name: 'BROWSER_CHOICE'
    }

    stages {

        stage('Run Test') {
            steps {
                sh "docker-compose up --pull=always"
                script {
                    def rerunExists = sh(script: '[ -f reports/rerun.txt ] &amp;amp;&amp;amp; [ -s reports/rerun.txt ]', returnStatus: true) == 0
                    if(rerunExists) {
                        error("Some tests failed.")
                    }
                }
            }
        }
    }

    post {
        always {
            sh "docker-compose down"
            archiveArtifacts artifacts: 'reports/*.html, reports/*.json', followSymlinks: false
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;agent any&lt;/code&gt;: Run pipeline on any available agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Parameters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;parameters&lt;/code&gt;: Allows users to provide input when triggering the pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;stage('Run Test')&lt;/code&gt;: Defines the stage for the pipeline.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;docker-compose up --pull=always&lt;/code&gt;: Launches the Dockerized testing environment defined in the docker-compose.yml file pulling the latest image.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;script&lt;/code&gt;: Uses a shell script to check if the reports/rerun.txt file exists and is non-empty. If its not, the pipeline fails with an error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Post&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;always&lt;/code&gt;: Defines actions that are always executed regardless of the pipeline's success or failure.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;docker-compose down&lt;/code&gt;: Shuts down the Docker containers and cleans up any associated resources.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;archiveArtifacts artifacts&lt;/code&gt;: Collects test reports (e.g., HTML and JSON files) from the reports directory and stores them in Jenkins as build artifacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now setup a github repository and push the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Tests in Jenkins
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Create New Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the Jenkins homepage and click on New Item.&lt;/li&gt;
&lt;li&gt;Enter an Item Name (e.g., Playwright-Docker-Runner) and select &lt;code&gt;Pipeline&lt;/code&gt; and click OK.&lt;/li&gt;
&lt;li&gt;Under Pipeline Definition, select &lt;code&gt;Pipeline script from SCM&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select SCM as &lt;code&gt;git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Provide the Repository URL for &lt;code&gt;playwright-runner&lt;/code&gt;. (&lt;em&gt;For private repositories, set up credentials for GitHub in Jenkins and select the credentials here.&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Specify the &lt;code&gt;Branch Specifier&lt;/code&gt; for your repository (e.g., &lt;code&gt;*/main&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Under Additional Behaviours, choose &lt;code&gt;Clean before checkout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set Script Path to &lt;code&gt;Jenkinsfile&lt;/code&gt;. (&lt;em&gt;ensure the Jenkinsfile is located in the root directory of your repository&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Click Save to create the pipeline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Run Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click Build Now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;The first build will run with the default parameters specified in the Jenkinsfile, as you will not be provided with parameter choices initially.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run with Parameters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  After the initial pipeline execution, navigate back to your created pipeline (Playwright-Docker-Runner).&lt;/li&gt;
&lt;li&gt;  Click Build with Parameters.&lt;/li&gt;
&lt;li&gt;  Select the desired parameters and run the pipeline. This will execute all tests based on the provided parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;View Build Artifacts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the pipeline is executed, you can check the build artifacts attached to the pipeline job.&lt;/p&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This guide has covered the complete setup to run Playwright tests in a Jenkins pipeline using Docker. From configuring Jenkins with Docker to creating pipelines for building and running tests, each step ensures a streamlined and scalable testing process. Using this setup, you can integrate your Playwright tests seamlessly into your CI/CD pipeline and maintain a robust testing environment.&lt;/p&gt;

</description>
      <category>jenkins</category>
      <category>playwright</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
